new: [webiste] query+config

pull/653/head
David Cruciani 2024-02-07 14:39:19 +01:00
parent b5b9d8d408
commit 2fe44f21fd
No known key found for this signature in database
GPG Key ID: 8690CDE1E3994B9B
2196 changed files with 144357 additions and 0 deletions

5
webiste/README.md Normal file
View File

@ -0,0 +1,5 @@
# PetitCrolle
Tool that help conducting a forensic analysis by providing a regroupement of tools in one place
<img title="MarkText logo" src="file:///home/dacru/Desktop/Git/PetitCrolle/doc/crolle.png" alt="Alt text" width="388" data-align="center">

38
webiste/app.py Normal file
View File

@ -0,0 +1,38 @@
from app import create_app, db
import argparse
from flask import render_template
import os
from app.utils.init_modules import create_modules_db
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--init_db", help="Initialise the db if it not exist", action="store_true")
parser.add_argument("-r", "--recreate_db", help="Delete and initialise the db", action="store_true")
parser.add_argument("-d", "--delete_db", help="Delete the db", action="store_true")
parser.add_argument("-m", "--create_module", help="Create modules in db", action="store_true")
args = parser.parse_args()
os.environ.setdefault('FLASKENV', 'development')
app = create_app()
@app.errorhandler(404)
def error_page_not_found(e):
return render_template('404.html'), 404
if args.init_db:
with app.app_context():
db.create_all()
elif args.recreate_db:
with app.app_context():
db.drop_all()
db.create_all()
elif args.delete_db:
with app.app_context():
db.drop_all()
elif args.create_module:
with app.app_context():
create_modules_db()
else:
app.run(host=app.config.get("FLASK_URL"), port=app.config.get("FLASK_PORT"))

30
webiste/app/__init__.py Normal file
View File

@ -0,0 +1,30 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import CSRFProtect
from flask_migrate import Migrate
from config import config as Config
import os
db = SQLAlchemy()
csrf = CSRFProtect()
migrate = Migrate()
def create_app():
app = Flask(__name__)
config_name = os.environ.get("FLASKENV")
app.config.from_object(Config[config_name])
Config[config_name].init_app(app)
db.init_app(app)
csrf.init_app(app)
migrate.init_app(app, db, render_as_batch=True)
from .home import home_blueprint
app.register_blueprint(home_blueprint, url_prefix="/")
return app

591
webiste/app/case/case.py Normal file
View File

@ -0,0 +1,591 @@
import json
from flask import Blueprint, render_template, redirect, jsonify, request, flash
from .form import CaseForm, CaseEditForm, AddOrgsCase, RecurringForm
from flask_login import login_required, current_user
from . import case_core as CaseModel
from . import common_core as CommonModel
from . import task_core as TaskModel
from ..db_class.db import Task_Template, Case_Template
from ..decorators import editor_required
from ..utils.utils import form_to_dict, check_tag
case_blueprint = Blueprint(
'case',
__name__,
template_folder='templates',
static_folder='static'
)
from .task import task_blueprint
case_blueprint.register_blueprint(task_blueprint)
##########
# Render #
##########
@case_blueprint.route("/", methods=['GET', 'POST'])
@login_required
def index():
"""List all cases"""
return render_template("case/case_index.html")
@case_blueprint.route("/create_case", methods=['GET', 'POST'])
@login_required
def create_case():
"""Create a case"""
form = CaseForm()
form.template_select.choices = [(template.id, template.title) for template in Case_Template.query.all()]
form.template_select.choices.insert(0, (0," "))
form.tasks_templates.choices = [(template.id, template.title) for template in Task_Template.query.all()]
form.tasks_templates.choices.insert(0, (0," "))
if form.validate_on_submit():
tag_list = request.form.getlist("tags_select")
cluster_list = request.form.getlist("clusters_select")
if CommonModel.check_tag(tag_list):
if CommonModel.check_cluster(cluster_list):
form_dict = form_to_dict(form)
form_dict["tags"] = tag_list
form_dict["clusters"] = cluster_list
case = CaseModel.create_case(form_dict, current_user)
flash("Case created", "success")
return redirect(f"/case/{case.id}")
return render_template("case/create_case.html", form=form)
return render_template("case/create_case.html", form=form)
return render_template("case/create_case.html", form=form)
@case_blueprint.route("/<cid>", methods=['GET', 'POST'])
@login_required
def view(cid):
"""View a case"""
case = CommonModel.get_case(cid)
if case:
present_in_case = CaseModel.get_present_in_case(cid, current_user)
return render_template("case/case_view.html", case=case.to_json(), present_in_case=present_in_case)
return render_template("404.html")
@case_blueprint.route("/edit/<cid>", methods=['GET','POST'])
@login_required
@editor_required
def edit_case(cid):
"""Edit the case"""
if CommonModel.get_case(cid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
form = CaseEditForm()
if form.validate_on_submit():
tag_list = request.form.getlist("tags_select")
cluster_list = request.form.getlist("clusters_select")
if CommonModel.check_tag(tag_list):
if CommonModel.check_cluster(cluster_list):
form_dict = form_to_dict(form)
form_dict["tags"] = tag_list
form_dict["clusters"] = cluster_list
CaseModel.edit_case(form_dict, cid, current_user)
flash("Case edited", "success")
return redirect(f"/case/{cid}")
return render_template("case/edit_case.html", form=form)
return render_template("case/edit_case.html", form=form)
else:
case_modif = CommonModel.get_case(cid)
form.description.data = case_modif.description
form.title.data = case_modif.title
form.deadline_date.data = case_modif.deadline
form.deadline_time.data = case_modif.deadline
return render_template("case/edit_case.html", form=form)
else:
flash("Access denied", "error")
else:
return render_template("404.html")
return redirect(f"/case/{id}")
@case_blueprint.route("/<cid>/add_orgs", methods=['GET', 'POST'])
@login_required
@editor_required
def add_orgs(cid):
"""Add orgs to the case"""
if CommonModel.get_case(cid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
form = AddOrgsCase()
case_org = CommonModel.get_org_in_case_by_case_id(cid)
org_list = list()
for org in CommonModel.get_org_order_by_name():
if case_org:
flag = False
for c_o in case_org:
if c_o.org_id == org.id:
flag = True
if not flag:
org_list.append((org.id, f"{org.name}"))
else:
org_list.append((org.id, f"{org.name}"))
form.org_id.choices = org_list
form.case_id.data = cid
if form.validate_on_submit():
form_dict = form_to_dict(form)
CaseModel.add_orgs_case(form_dict, cid, current_user)
flash("Orgs added", "success")
return redirect(f"/case/{cid}")
return render_template("case/add_orgs.html", form=form)
else:
flash("Access denied", "error")
else:
return render_template("404.html")
return redirect(f"/case/{cid}")
@case_blueprint.route("/<cid>/recurring", methods=['GET', 'POST'])
@login_required
@editor_required
def recurring(cid):
"""Recurring form"""
if CommonModel.get_case(cid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
form = RecurringForm()
form.case_id.data = cid
# List orgs and users in and verify if all users of an org are currently notify
orgs_in_case = CommonModel.get_orgs_in_case(cid)
orgs_to_return = list()
for org in orgs_in_case:
loc = org.to_json()
loc["users"] = list()
cp_checked_user = 0
cp_users = 0
for user in org.users:
cp_users += 1
loc_user = user.to_json()
if CommonModel.get_recu_notif_user(cid, user.id):
loc_user["checked"] = True
cp_checked_user += 1
else:
loc_user["checked"] = False
loc["users"].append(loc_user)
# if all users in an org are notify, then check the org checkbox
if cp_checked_user == cp_users:
loc["checked"] = True
else:
loc["checked"] = False
orgs_to_return.append(loc)
if form.validate_on_submit():
form_dict = form_to_dict(form)
if not CaseModel.change_recurring(form_dict, cid, current_user):
flash("Recurring empty", "error")
return redirect(f"/case/{cid}/recurring")
if not form_dict["remove"]:
CaseModel.notify_user_recurring(request.form.to_dict(), cid, orgs_in_case)
flash("Recurring set", "success")
return redirect(f"/case/{cid}")
return render_template("case/case_recurring.html", form=form, orgs=orgs_to_return)
flash("Action not allowed", "warning")
return redirect(f"/case/{cid}")
return render_template("404.html")
############
# Function #
# Route #
############
@case_blueprint.route("/get_cases_page", methods=['GET'])
@login_required
def get_cases():
"""Return all cases"""
page = request.args.get('page', 1, type=int)
tags = request.args.get('tags')
taxonomies = request.args.get('taxonomies')
or_and = request.args.get("or_and")
cases = CaseModel.sort_by_status(page, tags, taxonomies, or_and, completed=False)
role = CommonModel.get_role(current_user).to_json()
loc = CaseModel.regroup_case_info(cases, current_user)
return jsonify({"cases": loc["cases"], "role": role, "nb_pages": cases.pages}), 200
@case_blueprint.route("/search", methods=['GET'])
@login_required
def search():
"""Return cases matching search terms"""
text_search = request.args.get("text")
cases = CommonModel.search(text_search)
if cases:
return {"cases": [case.to_json() for case in cases]}, 200
return {"message": "No case", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/<cid>/delete", methods=['GET'])
@login_required
@editor_required
def delete(cid):
"""Delete the case"""
if CommonModel.get_case(cid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if CaseModel.delete_case(cid, current_user):
return {"message": "Case deleted", "toast_class": "success-subtle"}, 200
else:
return {"message": "Error case deleted", 'toast_class': "danger-subtle"}, 400
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "Case no found", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/<cid>/get_case_info", methods=['GET'])
@login_required
def get_case_info(cid):
"""Return all info of the case"""
case = CommonModel.get_case(cid)
if case:
tasks = TaskModel.sort_by_status_task_core(case, current_user, completed=False)
o_in_c = CommonModel.get_orgs_in_case(case.id)
orgs_in_case = [o_c.to_json() for o_c in o_in_c]
permission = CommonModel.get_role(current_user).to_json()
present_in_case = CaseModel.get_present_in_case(cid, current_user)
return jsonify({"case": case.to_json(), "tasks": tasks, "orgs_in_case": orgs_in_case, "permission": permission, "present_in_case": present_in_case, "current_user": current_user.to_json()}), 200
return {"message": "Case not found", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/<cid>/complete_case", methods=['GET'])
@login_required
@editor_required
def complete_case(cid):
"""Complete the case"""
if CommonModel.get_case(cid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if CaseModel.complete_case(cid, current_user):
flash("Case Completed")
if request.args.get('revived', 1) == "true":
return {"message": "Case Revived", "toast_class": "success-subtle"}, 200
return {"message": "Case completed", "toast_class": "success-subtle"}, 200
else:
if request.args.get('revived', 1) == "true":
return {"message": "Error case revived", 'toast_class': "danger-subtle"}, 400
return {"message": "Error case completed", 'toast_class': "danger-subtle"}, 400
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "Case not found", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/<cid>/remove_org/<oid>", methods=['GET'])
@login_required
@editor_required
def remove_org_case(cid, oid):
"""Remove an org to the case"""
if CommonModel.get_case(cid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if CaseModel.remove_org_case(cid, oid, current_user):
return {"message": "Org removed from case", "toast_class": "success-subtle"}, 200
return {"message": "Error removing org from case", "toast_class": "danger-subtle"}, 400
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "Case not found", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/<cid>/change_status", methods=['POST'])
@login_required
@editor_required
def change_status(cid):
"""Change the status of the case"""
status = request.json["status"]
case = CommonModel.get_case(cid)
if CommonModel.get_case(cid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
CaseModel.change_status_core(status, case, current_user)
return {"message": "Status changed", "toast_class": "success-subtle"}, 200
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "Case not found", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/get_status", methods=['GET'])
@login_required
def get_status():
"""Get status"""
status = CommonModel.get_all_status()
status_list = list()
for s in status:
status_list.append(s.to_json())
return jsonify({"status": status_list}), 200
@case_blueprint.route("/sort_by_ongoing", methods=['GET'])
@login_required
def sort_by_ongoing():
"""Sort Case by living one"""
page = request.args.get('page', 1, type=int)
tags = request.args.get('tags')
taxonomies = request.args.get('taxonomies')
or_and_taxo = request.args.get("or_and_taxo")
galaxies = request.args.get('galaxies')
clusters = request.args.get('clusters')
or_and_galaxies = request.args.get("or_and_galaxies")
cases_list = CaseModel.sort_by_status(page, taxonomies, galaxies, tags, clusters, or_and_taxo, or_and_galaxies, completed=False)
return CaseModel.regroup_case_info(cases_list, current_user)
@case_blueprint.route("/sort_by_finished", methods=['GET'])
@login_required
def sort_by_finished():
"""Sort Case by finished one"""
page = request.args.get('page', 1, type=int)
tags = request.args.get('tags')
taxonomies = request.args.get('taxonomies')
or_and_taxo = request.args.get("or_and_taxo")
galaxies = request.args.get('galaxies')
clusters = request.args.get('clusters')
or_and_galaxies = request.args.get("or_and_galaxies")
cases_list = CaseModel.sort_by_status(page, taxonomies, galaxies, tags, clusters, or_and_taxo, or_and_galaxies, completed=True)
return CaseModel.regroup_case_info(cases_list, current_user)
@case_blueprint.route("/ongoing", methods=['GET'])
@login_required
def ongoing_sort_by_filter():
"""Sort by filter for living case"""
page = request.args.get('page', 1, type=int)
filter = request.args.get('filter')
tags = request.args.get('tags')
taxonomies = request.args.get('taxonomies')
or_and_taxo = request.args.get("or_and_taxo")
galaxies = request.args.get('galaxies')
clusters = request.args.get('clusters')
or_and_galaxies = request.args.get("or_and_galaxies")
if filter:
cases_list, nb_pages = CaseModel.sort_by_filter(filter, page, taxonomies, galaxies, tags, clusters, or_and_taxo, or_and_galaxies, completed=False)
return CaseModel.regroup_case_info(cases_list, current_user, nb_pages)
return {"message": "No filter pass"}
@case_blueprint.route("/finished", methods=['GET'])
@login_required
def finished_sort_by_filter():
"""Sort by filter for finished task"""
page = request.args.get('page', 1, type=int)
filter = request.args.get('filter')
tags = request.args.get('tags')
taxonomies = request.args.get('taxonomies')
or_and_taxo = request.args.get("or_and_taxo")
galaxies = request.args.get('galaxies')
clusters = request.args.get('clusters')
or_and_galaxies = request.args.get("or_and_galaxies")
if filter:
cases_list, nb_pages = CaseModel.sort_by_filter(filter, page, taxonomies, galaxies, tags, clusters, or_and_taxo, or_and_galaxies, completed=True)
return CaseModel.regroup_case_info(cases_list, current_user, nb_pages)
return {"message": "No filter pass"}
@case_blueprint.route("/<cid>/get_all_users", methods=['GET'])
@login_required
def get_all_users(cid):
"""Get all user in case"""
case = CommonModel.get_case(cid)
if case:
users_list = list()
orgs = CommonModel.get_all_users_core(case)
for org in orgs:
for user in org.users:
if not user == current_user:
users_list.append(user.to_json())
return {"users_list": users_list}
return {"message": "Case not found"}, 404
@case_blueprint.route("/<cid>/get_assigned_users/<tid>", methods=['GET'])
@login_required
def get_assigned_users(cid, tid):
"""Get assigned users to the task"""
if CommonModel.get_case(cid):
users, _ = TaskModel.get_users_assign_task(tid, current_user)
return users
return {"message": "Case not found", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/<cid>/download", methods=['GET'])
@login_required
def download_case(cid):
"""Download a case"""
case = CommonModel.get_case(cid)
if case:
task_list = list()
for task in case.tasks:
task_list.append(task.download())
return_dict = case.download()
return_dict["tasks"] = task_list
return jsonify(return_dict), 200, {'Content-Disposition': f'attachment; filename=case_{case.title}.json'}
return {"message": "Case not found", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/<cid>/fork", methods=['POST'])
@login_required
def fork_case(cid):
"""Assign current user to the task"""
if CommonModel.get_case(cid):
if "case_title_fork" in request.json:
case_title_fork = request.json["case_title_fork"]
new_case = CaseModel.fork_case_core(cid, case_title_fork, current_user)
if type(new_case) == dict:
return new_case
return {"new_case_id": new_case.id}, 201
return {"message": "'case_title_fork' is missing", 'toast_class': "danger-subtle"}, 400
return {"message": "Case not found", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/get_all_case_title", methods=['GET'])
@login_required
def get_all_case_title():
data_dict = dict(request.args)
if CommonModel.get_case_by_title(data_dict["title"]):
flag = True
else:
flag = False
return {"title_already_exist": flag}
@case_blueprint.route("/<cid>/create_template", methods=['POST'])
@login_required
@editor_required
def create_template(cid):
if CommonModel.get_case(cid):
if "case_title_template" in request.json:
case_title_template = request.json["case_title_template"]
new_template = CaseModel.create_template_from_case(cid, case_title_template)
if type(new_template) == dict:
return new_template
return {"template_id": new_template.id}, 201
return {"message": "'case_title_template' is missing", 'toast_class': "danger-subtle"}, 400
return {"message": "Case not found", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/get_all_case_template_title", methods=['GET'])
@login_required
def get_all_case_template_title():
data_dict = dict(request.args)
if CommonModel.get_case_template_by_title(data_dict["title"]):
flag = True
else:
flag = False
return {"title_already_exist": flag}
@case_blueprint.route("/history/<cid>", methods=['GET'])
@login_required
def history(cid):
case = CommonModel.get_case(cid)
if case:
history = CommonModel.get_history(case.uuid)
if history:
return {"history": history}
return {"history": None}
return {"message": "Case Not found", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/get_taxonomies", methods=['GET'])
@login_required
def get_taxonomies():
return {"taxonomies": CommonModel.get_taxonomies()}, 200
@case_blueprint.route("/get_tags", methods=['GET'])
@login_required
def get_tags():
data_dict = dict(request.args)
if "taxonomies" in data_dict:
taxos = json.loads(data_dict["taxonomies"])
return {"tags": CommonModel.get_tags(taxos)}, 200
return {"message": "'taxonomies' is missing", 'toast_class': "warning-subtle"}, 400
@case_blueprint.route("/get_taxonomies_case/<cid>", methods=['GET'])
@login_required
def get_taxonomies_case(cid):
case = CommonModel.get_case(cid)
if case:
tags = CommonModel.get_case_tags(case.id)
taxonomies = []
if tags:
taxonomies = [tag.split(":")[0] for tag in tags]
return {"tags": tags, "taxonomies": taxonomies}
return {"message": "Case Not found", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/get_galaxies", methods=['GET'])
@login_required
def get_galaxies():
return {"galaxies": CommonModel.get_galaxies()}, 200
@case_blueprint.route("/get_clusters", methods=['GET'])
@login_required
def get_clusters():
if "galaxies" in request.args:
galaxies = request.args.get("galaxies")
galaxies = json.loads(galaxies)
return {"clusters": CommonModel.get_clusters_galaxy(galaxies)}, 200
return {"message": "'galaxies' is missing", 'toast_class': "warning-subtle"}, 400
@case_blueprint.route("/get_galaxies_case/<cid>", methods=['GET'])
@login_required
def get_galaxies_case(cid):
case = CommonModel.get_case(cid)
if case:
clusters = CommonModel.get_case_clusters(case.id)
galaxies = []
if clusters:
for cluster in clusters:
loc_g = CommonModel.get_galaxy(cluster.galaxy_id)
if not loc_g.name in galaxies:
galaxies.append(loc_g.name)
index = clusters.index(cluster)
clusters[index] = cluster.tag
return {"clusters": clusters, "galaxies": galaxies}
return {"message": "Case Not found", 'toast_class': "danger-subtle"}, 404
@case_blueprint.route("/get_modules", methods=['GET'])
@login_required
def get_modules():
return {"modules": CaseModel.get_modules()}, 200
# return {"message": "'galaxies' is missing", 'toast_class': "warning-subtle"}, 400
@case_blueprint.route("/get_instance_module", methods=['GET'])
@login_required
def get_instance_module():
if "module" in request.args:
module = request.args.get("module")
return {"instances": CaseModel.get_instance_module_core(module, current_user.id)}, 200

View File

@ -0,0 +1,602 @@
from flask import Blueprint, request
from . import case_core as CaseModel
from . import common_core as CommonModel
from . import task_core as TaskModel
from . import case_core_api as CaseModelApi
from flask_restx import Api, Resource
from ..decorators import api_required, editor_required
api_case_blueprint = Blueprint('api_case', __name__)
api = Api(api_case_blueprint,
title='Flowintel-cm API',
description='API to manage a case management instance.',
version='0.1',
default='GenericAPI',
default_label='Generic Flowintel-cm API',
doc='/doc/'
)
@api.route('/all')
@api.doc(description='Get all cases')
class GetCases(Resource):
method_decorators = [api_required]
def get(self):
cases = CommonModel.get_all_cases()
return {"cases": [case.to_json() for case in cases]}, 200
@api.route('/not_completed')
@api.doc(description='Get all not completed cases')
class GetCases_not_completed(Resource):
method_decorators = [api_required]
def get(self):
cases = CommonModel.get_case_by_completed(False)
return {"cases": [case.to_json() for case in cases]}, 200
@api.route('/completed')
@api.doc(description='Get all completed cases')
class GetCases_not_completed(Resource):
method_decorators = [api_required]
def get(self):
cases = CommonModel.get_case_by_completed(True)
return {"cases": [case.to_json() for case in cases]}, 200
@api.route('/<cid>')
@api.doc(description='Get a case', params={'cid': 'id of a case'})
class GetCase(Resource):
method_decorators = [api_required]
def get(self, cid):
case = CommonModel.get_case(cid)
if case:
case_json = case.to_json()
orgs = CommonModel.get_orgs_in_case(cid)
case_json["orgs"] = list()
for org in orgs:
case_json["orgs"].append({"id": org.id, "uuid": org.uuid, "name": org.name})
return case_json, 200
return {"message": "Case not found"}, 404
@api.route('/title', methods=["POST"])
@api.doc(description='Get a case by title')
class GetCaseTitle(Resource):
method_decorators = [api_required]
@api.doc(params={"title": "Title of a case"})
def post(self):
if "title" in request.json:
case = CommonModel.get_case_by_title(request.json["title"])
if case:
case_json = case.to_json()
orgs = CommonModel.get_orgs_in_case(case.id)
case_json["orgs"] = [{"id": org.id, "uuid": org.uuid, "name": org.name} for org in orgs]
return case_json, 200
return {"message": "Case not found"}, 404
return {"message": "Need to pass a title"}, 404
@api.route('/<cid>/complete')
@api.doc(description='Complete a case', params={'cid': 'id of a case'})
class CompleteCase(Resource):
method_decorators = [editor_required, api_required]
def get(self, cid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
case = CommonModel.get_case(cid)
if case:
if CaseModel.complete_case(cid, current_user):
return {"message": f"Case {cid} completed"}, 200
return {"message": f"Error case {cid} completed"}, 400
return {"message": "Case not found"}, 404
return {"message": "Permission denied"}, 403
@api.route('/<cid>/create_template', methods=["POST"])
@api.doc(description='Create a template form case', params={'cid': 'id of a case'})
class CreateTemplate(Resource):
method_decorators = [editor_required, api_required]
@api.doc(params={"title_template": "Title for the template that will be create"})
def post(self, cid):
if "title_template" in request.json:
if CommonModel.get_case(cid):
new_template = CaseModel.create_template_from_case(cid, request.json["title_template"])
if type(new_template) == dict:
return new_template
return {"template_id": new_template.id}, 201
return {"message": "Case not found"}, 404
return {"message": "'title_template' is missing"}, 400
@api.route('/<cid>/recurring', methods=['POST'])
@api.doc(description='Set a case recurring')
class RecurringCase(Resource):
method_decorators = [editor_required, api_required]
@api.doc(params={
"once": "Date(%Y-%m-%d)",
"daily": "Boolean",
"weekly": "Date(%Y-%m-%d). Start date.",
"monthly": "Date(%Y-%m-%d). Start date.",
"remove": "Boolean"
})
def post(self, cid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if request.json:
verif_dict = CaseModelApi.verif_set_recurring(request.json)
if "message" not in verif_dict:
CaseModel.change_recurring(verif_dict, cid, current_user)
return {"message": "Recurring changed"}, 200
return verif_dict
return {"message": "Please give data"}, 400
return {"message": "Permission denied"}, 403
@api.route('/<cid>/tasks')
@api.doc(description='Get all tasks for a case', params={'cid': 'id of a case'})
class GetTasks(Resource):
method_decorators = [api_required]
def get(self, cid):
case = CommonModel.get_case(cid)
if case:
tasks = list()
for task in case.tasks:
tasks.append(task.to_json())
return tasks, 200
return {"message": "Case not found"}, 404
@api.route('/<cid>/task/<tid>')
@api.doc(description='Get a specific task for a case', params={"cid": "id of a case", "tid": "id of a task"})
class GetTask(Resource):
method_decorators = [api_required]
def get(self, cid, tid):
task = CommonModel.get_task(tid)
if task:
if int(cid) == task.case_id:
loc = dict()
loc["users_assign"], loc["is_current_user_assign"] = TaskModel.get_users_assign_task(task.id, CaseModelApi.get_user_api(request.headers["X-API-KEY"]))
loc["task"] = task.to_json()
return loc, 200
else:
return {"message": "Task not in this case"}, 404
return {"message": "Task not found"}, 404
@api.route('/<cid>/delete')
@api.doc(description='Delete a case', params={'cid': 'id of a case'})
class DeleteCase(Resource):
method_decorators = [editor_required, api_required]
def get(self, cid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if CaseModel.delete_case(cid, current_user):
return {"message": "Case deleted"}, 200
return {"message": "Error case deleted"}, 400
return {"message": "Permission denied"}, 403
@api.route('/<cid>/task/<tid>/delete')
@api.doc(description='Delete a specific task in a case', params={'cid': 'id of a case', "tid": "id of a task"})
class DeleteTask(Resource):
method_decorators = [editor_required, api_required]
def get(self, cid, tid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
task = CommonModel.get_task(tid)
if task:
if int(cid) == task.case_id:
if TaskModel.delete_task(tid, current_user):
return {"message": "Task deleted"}, 200
else:
return {"message": "Error task deleted"}, 400
else:
return {"message": "Task not in this case"}, 404
return {"message": "Task not found"}, 404
return {"message": "Permission denied"}, 403
@api.route('/create', methods=['POST'])
@api.doc(description='Create a case')
class CreateCase(Resource):
method_decorators = [editor_required, api_required]
@api.doc(params={
"title": "Required. Title for a case",
"description": "Description of a case",
"deadline_date": "Date(%Y-%m-%d)",
"deadline_time": "Time(%H-%M)"
})
def post(self):
user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if request.json:
verif_dict = CaseModelApi.verif_create_case_task(request.json, True)
if "message" not in verif_dict:
case = CaseModel.create_case(verif_dict, user)
return {"message": f"Case created, id: {case.id}"}, 201
return verif_dict, 400
return {"message": "Please give data"}, 400
@api.route('/<cid>/create_task', methods=['POST'])
@api.doc(description='Create a new task to a case', params={'cid': 'id of a case'})
class CreateTask(Resource):
method_decorators = [editor_required, api_required]
@api.doc(params={
"title": "Required. Title for a task",
"description": "Description of a task",
"url": "Link to a tool or a ressource",
"deadline_date": "Date(%Y-%m-%d)",
"deadline_time": "Time(%H-%M)"
})
def post(self, cid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if request.json:
verif_dict = CaseModelApi.verif_create_case_task(request.json, False)
if "message" not in verif_dict:
task = TaskModel.create_task(verif_dict, cid, current_user)
return {"message": f"Task created for case id: {cid}"}, 201
return verif_dict, 400
return {"message": "Please give data"}, 400
return {"message": "Permission denied"}, 403
@api.route('/<id>/edit', methods=['POST'])
@api.doc(description='Edit a case', params={'id': 'id of a case'})
class EditCase(Resource):
method_decorators = [editor_required, api_required]
@api.doc(params={"title": "Title for a case", "description": "Description of a case", "deadline_date": "Date(%Y-%m-%d)", "deadline_time": "Time(%H-%M)"})
def post(self, id):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(id, current_user) or current_user.is_admin():
if request.json:
verif_dict = CaseModelApi.verif_edit_case(request.json, id)
if "message" not in verif_dict:
CaseModel.edit_case(verif_dict, id, current_user)
return {"message": f"Case {id} edited"}, 200
return verif_dict, 400
return {"message": "Please give data"}, 400
return {"message": "Permission denied"}, 403
@api.route('/<cid>/task/<tid>/edit', methods=['POST'])
@api.doc(description='Edit a task in a case', params={'cid': 'id of a case', "tid": "id of a task"})
class EditTake(Resource):
method_decorators = [editor_required, api_required]
@api.doc(params={"title": "Title for a case", "description": "Description of a case", "deadline_date": "Date(%Y-%m-%d)", "deadline_time": "Time(%H-%M)"})
def post(self, cid, tid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if request.json:
task = CommonModel.get_task(tid)
if task:
if int(cid) == task.case_id:
verif_dict = CaseModelApi.verif_edit_task(request.json, tid)
if "message" not in verif_dict:
TaskModel.edit_task_core(verif_dict, tid, current_user)
return {"message": f"Task {tid} edited"}, 200
return verif_dict, 400
else:
return {"message": "Task not in this case"}, 404
else:
return {"message": "Task not found"}, 404
return {"message": "Please give data"}, 400
return {"message": "Permission denied"}, 403
@api.route('/<cid>/task/<tid>/complete')
@api.doc(description='Complete a task in a case', params={'cid': 'id of a case', "tid": "id of a task"})
class CompleteTake(Resource):
method_decorators = [editor_required, api_required]
def get(self, cid, tid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
task = CommonModel.get_task(tid)
if task:
if int(cid) == task.case_id:
if TaskModel.complete_task(tid, current_user):
return {"message": f"Task {tid} completed"}, 200
return {"message": f"Error task {tid} completed"}, 400
else:
return {"message": "Task not in this case"}, 404
return {"message": "Task not found"}, 404
return {"message": "Permission denied"}, 403
@api.route('/<cid>/task/<tid>/get_note')
@api.doc(description='Get note of a task in a case', params={'cid': 'id of a case', "tid": "id of a task"})
class GetNoteTask(Resource):
method_decorators = [api_required]
def get(self, cid, tid):
task = CommonModel.get_task(tid)
if task:
if int(cid) == task.case_id:
return {"note": task.notes}, 200
else:
return {"message": "Task not in this case"}, 404
return {"message": "Task not found"}, 404
@api.route('/<cid>/task/<tid>/modif_note', methods=['POST'])
@api.doc(description='Edit note of a task in a case', params={'cid': 'id of a case', "tid": "id of a task"})
class ModifNoteTask(Resource):
method_decorators = [editor_required, api_required]
@api.doc(params={"note": "note to create or modify"})
def post(self, cid, tid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if "note" in request.json:
task = CommonModel.get_task(tid)
if task:
if int(cid) == task.case_id:
if TaskModel.modif_note_core(tid, current_user, request.json["note"]):
return {"message": f"Note for task {tid} edited"}, 200
return {"message": f"Error Note for task {tid} edited"}, 400
else:
return {"message": "Task not in this case"}, 404
return {"message": "Task not found"}, 404
return {"message": "Key 'note' not found"}, 400
return {"message": "Permission denied"}, 403
@api.route('/<cid>/add_org', methods=['POST'])
@api.doc(description='Add an org to the case', params={'cid': 'id of a case'})
class AddOrgCase(Resource):
method_decorators = [editor_required, api_required]
@api.doc(params={"name": "Name of the organisation", "oid": "id of the organisation"})
def post(self, cid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if "name" in request.json:
org = CommonModel.get_org_by_name(request.json["name"])
elif "oid" in request.json:
org = CommonModel.get_org(request.json["oid"])
else:
return {"message": "Required an id or a name of an Org"}, 400
if org:
if not CommonModel.get_org_in_case(org.id, cid):
if CaseModel.add_orgs_case({"org_id": [org.id]}, cid, current_user):
return {"message": f"Org added to case {cid}"}, 200
return {"message": f"Error Org added to case {cid}"}, 400
return {"message": "Org already in case"}, 400
return {"message": "Org not found"}, 404
return {"message": "Permission denied"}, 403
@api.route('/<cid>/remove_org/<oid>', methods=['GET'])
@api.doc(description='Add an org to the case', params={'cid': 'id of a case', "oid": "id of an org"})
class RemoveOrgCase(Resource):
method_decorators = [editor_required, api_required]
def get(self, cid, oid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
org = CommonModel.get_org(oid)
if org:
if CommonModel.get_org_in_case(org.id, cid):
if CaseModel.remove_org_case(cid, org.id, current_user):
return {"message": f"Org deleted from case {cid}"}, 200
return {"message": f"Error Org deleted from case {cid}"}, 400
return {"message": "Org not in case"}, 404
return {"message": "Org not found"}, 404
return {"message": "Permission denied"}, 403
@api.route('/<cid>/take_task/<tid>', methods=['GET'])
@api.doc(description='Assign current user to the task', params={'cid': 'id of a case', "tid": "id of a task"})
class AssignTask(Resource):
method_decorators = [editor_required, api_required]
def get(self, cid, tid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
task = CommonModel.get_task(tid)
if task:
if int(cid) == task.case_id:
if TaskModel.assign_task(tid, user=current_user, current_user=current_user, flag_current_user=True):
return {"message": f"Task Take"}, 200
return {"message": f"Error Task Take"}, 400
return {"message": "Task not in this case"}, 404
return {"message": "Task not found"}, 404
return {"message": "Permission denied"}, 403
@api.route('/<cid>/remove_assignment/<tid>', methods=['GET'])
@api.doc(description='Remove assigment of current user to the task', params={'cid': 'id of a case', "tid": "id of a task"})
class RemoveOrgCase(Resource):
method_decorators = [editor_required, api_required]
def get(self, cid, tid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
task = CommonModel.get_task(tid)
if task:
if int(cid) == task.case_id:
if TaskModel.remove_assign_task(tid, user=current_user, current_user=current_user, flag_current_user=True):
return {"message": f"Removed from assignment"}, 200
return {"message": f"Error Removed from assignment"}, 400
return {"message": "Task not in this case"}, 404
return {"message": "Task not found"}, 404
return {"message": "Permission denied"}, 403
@api.route('/<cid>/get_all_users', methods=['GET'])
@api.doc(description='Get list of user that can be assign', params={'cid': 'id of a case'})
class GetAllUsers(Resource):
method_decorators = [api_required]
def get(self, cid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
case = CommonModel.get_case(cid)
if case:
users_list = list()
for org in CommonModel.get_all_users_core(case):
for user in org.users:
if not user == current_user:
users_list.append(user.to_json())
return {"users": users_list}, 200
return {"message": "Case not found"}, 404
@api.route('/<cid>/task/<tid>/assign_users', methods=['POST'])
@api.doc(description='Assign users to a task', params={'cid': 'id of a case', "tid": "id of a task"})
class AssignUser(Resource):
method_decorators = [editor_required, api_required]
@api.doc(params={"users_id": "List of user id"})
def post(self, cid, tid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
task = CommonModel.get_task(tid)
if task:
if int(cid) == task.case_id:
users_list = request.json["users_id"]
for user in users_list:
TaskModel.assign_task(tid, user=user, current_user=current_user, flag_current_user=False)
return {"message": "Users Assigned"}, 200
return {"message": "Task not in this case"}, 404
return {"message": "Task not found"}, 404
return {"message": "Permission denied"}, 403
@api.route('/<cid>/task/<tid>/remove_assign_user', methods=['POST'])
@api.doc(description='Remove an assign user to a task', params={'cid': 'id of a case', "tid": "id of a task"})
class AssignUser(Resource):
method_decorators = [editor_required, api_required]
@api.doc(params={"user_id": "Id of a user"})
def post(self, cid, tid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
task = CommonModel.get_task(tid)
if task:
if int(cid) == task.case_id:
user_id = request.json["user_id"]
if TaskModel.remove_assign_task(tid, user=user_id, current_user=current_user, flag_current_user=False):
return {"message": "User Removed from assignment"}, 200
return {"message": "Task not in this case"}, 404
return {"message": "Task not found"}, 404
return {"message": "Permission denied"}, 403
@api.route('/<cid>/task/<tid>/change_status', methods=['POST'])
@api.doc(description='Change status of a task', params={'cid': 'id of a case', "tid": "id of a task"})
class ChangeStatus(Resource):
method_decorators = [editor_required, api_required]
@api.doc(params={"status_id": "Id of the new status"})
def post(self, cid, tid):
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
task = CommonModel.get_task(tid)
if task:
if int(cid) == task.case_id:
if TaskModel.change_task_status(request.json["status_id"], task, current_user):
return {"message": "Status changed"}, 200
return {"message": "Task not in this case"}, 404
return {"message": "Task not found"}, 404
return {"message": "Permission denied"}, 403
@api.route('/list_status', methods=['GET'])
@api.doc(description='List all status')
class ChangeStatus(Resource):
method_decorators = [api_required]
def get(self):
return [status.to_json() for status in CommonModel.get_all_status()], 200
@api.route('/<cid>/history', methods=['GET'])
@api.doc(description='Get history of a case', params={'cid': 'id of a case'})
class ChangeStatus(Resource):
method_decorators = [api_required]
def get(self, cid):
case = CommonModel.get_case(cid)
if case:
history = CommonModel.get_history(case.uuid)
if history:
return {"history": history}
return {"history": None}
return {"message": "Case Not found"}, 404
@api.route('/<cid>/task/<tid>/files')
@api.doc(description='Get list of files', params={"cid": "id of a case", "tid": "id of a task"})
class DownloadFile(Resource):
method_decorators = [api_required]
def get(self, cid, tid):
case = CommonModel.get_case(cid)
if case:
task = CommonModel.get_task(tid)
if task:
file_list = [file.to_json() for file in task.files]
return {"files": file_list}, 200
return {"message": "Task Not found"}, 404
return {"message": "Case Not found"}, 404
@api.route('/<cid>/task/<tid>/upload_file')
@api.doc(description='Upload a file')
class UploadFile(Resource):
method_decorators = [api_required]
@api.doc(params={})
def post(self, cid, tid):
case = CommonModel.get_case(cid)
if case:
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
task = CommonModel.get_task(tid)
if task:
if TaskModel.add_file_core(task, request.files, current_user):
return {"message": "File added"}, 200
return {"message": "Task Not found"}, 404
return {"message": "Permission denied"}, 403
return {"message": "Case Not found"}, 404
@api.route('/<cid>/task/<tid>/download_file/<fid>')
@api.doc(description='Download a file', params={"cid": "id of a case", "tid": "id of a task", "fid": "id of a file"})
class DownloadFile(Resource):
method_decorators = [api_required]
def get(self, cid, tid, fid):
case = CommonModel.get_case(cid)
if case:
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
task = CommonModel.get_task(tid)
if task:
file = CommonModel.get_file(fid)
if file and file in task.files:
return TaskModel.download_file(file)
return {"message": "Task Not found"}, 404
return {"message": "Permission denied"}, 403
return {"message": "Case Not found"}, 404
@api.route('/<cid>/task/<tid>/delete_file/<fid>')
@api.doc(description='Delete a file', params={"cid": "id of a case", "tid": "id of a task", "fid": "id of a file"})
class DeleteFile(Resource):
method_decorators = [api_required]
@api.doc(params={
})
def get(self, cid, tid, fid):
case = CommonModel.get_case(cid)
if case:
current_user = CaseModelApi.get_user_api(request.headers["X-API-KEY"])
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
task = CommonModel.get_task(tid)
if task:
file = CommonModel.get_file(fid)
if file and file in task.files:
if TaskModel.delete_file(file, task, current_user):
return {"message": "File Deleted"}, 200
return {"message": "Task Not found"}, 404
return {"message": "Permission denied"}, 403
return {"message": "Case Not found"}, 404

View File

@ -0,0 +1,610 @@
import os
import ast
import uuid
import datetime
from app.utils.utils import get_modules_list, MODULES, MODULES_CONFIG
from .. import db
from ..db_class.db import *
from sqlalchemy import desc, and_
from ..notification import notification_core as NotifModel
from dateutil import relativedelta
from ..tools.tools_core import create_case_from_template
from . import common_core as CommonModel
from . import task_core as TaskModel
def delete_case(cid, current_user):
"""Delete a case by is id"""
case = CommonModel.get_case(cid)
if case:
# Delete all tasks in the case
for task in case.tasks:
TaskModel.delete_task(task.id, current_user)
history_path = os.path.join(CommonModel.HISTORY_FOLDER, str(case.uuid))
if os.path.isfile(history_path):
try:
os.remove(history_path)
except:
return False
NotifModel.create_notification_all_orgs(f"Case: '{case.id}-{case.title}' was deleted", cid, html_icon="fa-solid fa-trash", current_user=current_user)
Case_Tags.query.filter_by(case_id=case.id).delete()
Case_Galaxy_Tags.query.filter_by(case_id=case.id).delete()
Case_Org.query.filter_by(case_id=case.id).delete()
db.session.delete(case)
db.session.commit()
return True
return False
def complete_case(cid, current_user):
"""Complete case by is id"""
case = CommonModel.get_case(cid)
if case is not None:
case.completed = not case.completed
if case.completed:
case.status_id = Status.query.filter_by(name="Finished").first().id
for task in case.tasks:
TaskModel.complete_task(task.id, current_user)
NotifModel.create_notification_all_orgs(f"Case: '{case.id}-{case.title}' is now completed", cid, html_icon="fa-solid fa-square-check", current_user=current_user)
else:
case.status_id = Status.query.filter_by(name="Created").first().id
NotifModel.create_notification_all_orgs(f"Case: '{case.id}-{case.title}' is now revived", cid, html_icon="fa-solid fa-heart-circle-plus", current_user=current_user)
CommonModel.update_last_modif(cid)
db.session.commit()
CommonModel.save_history(case.uuid, current_user, "Case completed")
return True
return False
def create_case(form_dict, user):
"""Add a case to the DB"""
if "template_select" in form_dict and not 0 in form_dict["template_select"]:
for template in form_dict["template_select"]:
if Case_Template.query.get(template):
case = create_case_from_template(template, form_dict["title_template"], user)
else:
deadline = CommonModel.deadline_check(form_dict["deadline_date"], form_dict["deadline_time"])
case = Case(
title=form_dict["title"],
description=form_dict["description"],
uuid=str(uuid.uuid4()),
creation_date=datetime.datetime.now(tz=datetime.timezone.utc),
last_modif=datetime.datetime.now(tz=datetime.timezone.utc),
deadline=deadline,
status_id=1,
owner_org_id=user.org_id
)
db.session.add(case)
db.session.commit()
for tags in form_dict["tags"]:
tag = CommonModel.get_tag(tags)
case_tag = Case_Tags(
tag_id=tag.id,
case_id=case.id
)
db.session.add(case_tag)
db.session.commit()
for clusters in form_dict["clusters"]:
cluster = CommonModel.get_cluster_by_name(clusters)
case_galaxy = Case_Galaxy_Tags(
cluster_id=cluster.id,
case_id=case.id
)
db.session.add(case_galaxy)
db.session.commit()
if "tasks_templates" in form_dict and not 0 in form_dict["tasks_templates"]:
for tid in form_dict["tasks_templates"]:
task = Task_Template.query.get(tid)
t = Task(
uuid=str(uuid.uuid4()),
title=task.title,
description=task.description,
url=task.url,
creation_date=datetime.datetime.now(tz=datetime.timezone.utc),
last_modif=datetime.datetime.now(tz=datetime.timezone.utc),
case_id=case.id,
status_id=1
)
db.session.add(t)
db.session.commit()
for t_t in Task_Template_Tags.query.filter_by(task_id=task.id).all():
task_tag = Task_Tags(
task_id=t.id,
tag_id=t_t.tag_id
)
db.session.add(task_tag)
db.session.commit()
for t_t in Task_Template_Galaxy_Tags.query.filter_by(task_id=task.id).all():
task_galaxy = Task_Galaxy_Tags(
task_id=t.id,
cluster_id=t_t.cluster_id
)
db.session.add(task_galaxy)
db.session.commit()
# Add the current user's org to the case
case_org = Case_Org(
case_id=case.id,
org_id=user.org_id
)
db.session.add(case_org)
db.session.commit()
CommonModel.save_history(case.uuid, user, "Case Created")
return case
def edit_case(form_dict, cid, current_user):
"""Edit a case to the DB"""
case = CommonModel.get_case(cid)
deadline = CommonModel.deadline_check(form_dict["deadline_date"], form_dict["deadline_time"])
case.title = form_dict["title"]
case.description=form_dict["description"]
case.deadline=deadline
## Tags
case_tag_db = Case_Tags.query.filter_by(case_id=case.id).all()
for tags in form_dict["tags"]:
tag = CommonModel.get_tag(tags)
if not tags in case_tag_db:
case_tag = Case_Tags(
tag_id=tag.id,
case_id=case.id
)
db.session.add(case_tag)
db.session.commit()
for c_t_db in case_tag_db:
if not c_t_db in form_dict["tags"]:
Case_Tags.query.filter_by(id=c_t_db.id).delete()
db.session.commit()
## Clusters
case_cluster_db = Case_Galaxy_Tags.query.filter_by(case_id=case.id).all()
for clusters in form_dict["clusters"]:
cluster = CommonModel.get_cluster_by_name(clusters)
if not clusters in case_cluster_db:
case_galaxy_tag = Case_Galaxy_Tags(
cluster_id=cluster.id,
case_id=case.id
)
db.session.add(case_galaxy_tag)
db.session.commit()
for c_t_db in case_cluster_db:
if not c_t_db in form_dict["clusters"]:
Case_Galaxy_Tags.query.filter_by(id=c_t_db.id).delete()
db.session.commit()
CommonModel.update_last_modif(cid)
db.session.commit()
CommonModel.save_history(case.uuid, current_user, f"Case edited")
def add_orgs_case(form_dict, cid, current_user):
"""Add orgs to case in th DB"""
for org_id in form_dict["org_id"]:
case_org = Case_Org(
case_id=cid,
org_id=org_id
)
db.session.add(case_org)
case = CommonModel.get_case(cid)
NotifModel.create_notification_org(f"{CommonModel.get_org(org_id).name} add to case: '{case.id}-{case.title}'", cid, org_id, html_icon="fa-solid fa-sitemap", current_user=current_user)
CommonModel.update_last_modif(cid)
db.session.commit()
case = CommonModel.get_case(cid)
CommonModel.save_history(case.uuid, current_user, f"Org {org_id} added")
return True
def remove_org_case(case_id, org_id, current_user):
"""Remove an org from a case"""
case_org = Case_Org.query.filter_by(case_id=case_id, org_id=org_id).first()
if case_org:
db.session.delete(case_org)
case = CommonModel.get_case(case_id)
NotifModel.create_notification_org(f"{CommonModel.get_org(org_id).name} removed from case: '{case.id}-{case.title}'", case_id, org_id, html_icon="fa-solid fa-door-open", current_user=current_user)
CommonModel.update_last_modif(case_id)
db.session.commit()
case = CommonModel.get_case(case_id)
CommonModel.save_history(case.uuid, current_user, f"Org {org_id} removed")
return True
return False
def get_present_in_case(case_id, user):
"""Return if current user is present in a case"""
orgs_in_case = CommonModel.get_orgs_in_case(case_id)
present_in_case = False
for org in orgs_in_case:
if org.id == user.org_id:
present_in_case = True
return present_in_case
def change_status_core(status, case, current_user):
case.status_id = status
CommonModel.update_last_modif(case.id)
db.session.commit()
CommonModel.save_history(case.uuid, current_user, "Case Status changed")
return True
def regroup_case_info(cases, user, nb_pages=None):
loc = dict()
loc["cases"] = list()
for case in cases:
present_in_case = get_present_in_case(case.id, user)
case_loc = case.to_json()
case_loc["present_in_case"] = present_in_case
case_loc["current_user_permission"] = CommonModel.get_role(user).to_json()
loc["cases"].append(case_loc)
if nb_pages:
loc["nb_pages"] = nb_pages
else:
try:
loc["nb_pages"] = cases.pages
except:
pass
return loc
def build_case_query(page, completed, tags=None, taxonomies=None, galaxies=None, clusters=None, filter=None):
query = Case.query
conditions = [Case.completed == completed]
if tags or taxonomies:
query = query.join(Case_Tags, Case_Tags.case_id == Case.id)
query = query.join(Tags, Case_Tags.tag_id == Tags.id)
if tags:
tags = ast.literal_eval(tags)
conditions.append(Tags.name.in_(list(tags)))
if taxonomies:
taxonomies = ast.literal_eval(taxonomies)
query = query.join(Taxonomy, Taxonomy.id == Tags.taxonomy_id)
conditions.append(Taxonomy.name.in_(list(taxonomies)))
if clusters or galaxies:
query = query.join(Case_Galaxy_Tags, Case_Galaxy_Tags.case_id == Case.id)
query = query.join(Cluster, Case_Galaxy_Tags.cluster_id == Cluster.id)
if clusters:
clusters = ast.literal_eval(clusters)
conditions.append(Cluster.name.in_(list(clusters)))
if galaxies:
galaxies = ast.literal_eval(galaxies)
query = query.join(Galaxy, Galaxy.id == Cluster.galaxy_id)
conditions.append(Galaxy.name.in_(list(galaxies)))
if filter:
query.order_by(desc(filter))
return query.filter(and_(*conditions)).paginate(page=page, per_page=25, max_per_page=50)
def sort_by_status(page, taxonomies=[], galaxies=[], tags=[], clusters=[], or_and_taxo="true", or_and_galaxies="true", completed=False):
cases = build_case_query(page, completed, tags, taxonomies, galaxies, clusters)
if tags:
tags = ast.literal_eval(tags)
if taxonomies:
taxonomies = ast.literal_eval(taxonomies)
if galaxies:
galaxies = ast.literal_eval(galaxies)
if clusters:
clusters = ast.literal_eval(clusters)
if tags or taxonomies or galaxies or clusters:
if or_and_taxo == "false":
glob_list = []
for case in cases:
tags_db = case.to_json()["tags"]
loc_tag = [tag["name"] for tag in tags_db]
taxo_list = [Taxonomy.query.get(tag["taxonomy_id"]).name for tag in tags_db]
if (not tags or all(item in loc_tag for item in tags)) and \
(not taxonomies or all(item in taxo_list for item in taxonomies)):
glob_list.append(case)
cases = glob_list
if or_and_galaxies == "false":
glob_list = []
for case in cases:
clusters_db = case.to_json()["clusters"]
loc_cluster = [cluster["name"] for cluster in clusters_db]
galaxies_list = [Galaxy.query.get(cluster["galaxy_id"]).name for cluster in clusters_db]
if (not clusters or all(item in loc_cluster for item in clusters)) and \
(not galaxies or all(item in galaxies_list for item in galaxies)):
glob_list.append(case)
cases = glob_list
else:
cases = Case.query.filter_by(completed=completed).paginate(page=page, per_page=25, max_per_page=50)
return cases
def sort_by_filter(filter, page, taxonomies=[], galaxies=[], tags=[], clusters=[], or_and_taxo="true", or_and_galaxies="true", completed=False):
cases = build_case_query(page, completed, tags, taxonomies, galaxies, clusters, filter)
nb_pages = cases.pages
if tags:
tags = ast.literal_eval(tags)
if taxonomies:
taxonomies = ast.literal_eval(taxonomies)
if galaxies:
galaxies = ast.literal_eval(galaxies)
if clusters:
clusters = ast.literal_eval(clusters)
if tags or taxonomies or galaxies or clusters:
if or_and_taxo == "false":
glob_list = []
for case in cases:
tags_db = case.to_json()["tags"]
loc_tag = [tag["name"] for tag in tags_db]
taxo_list = [Taxonomy.query.get(tag["taxonomy_id"]).name for tag in tags_db]
if (not tags or all(item in loc_tag for item in tags)) and \
(not taxonomies or all(item in taxo_list for item in taxonomies)):
glob_list.append(case)
cases = glob_list
if or_and_galaxies == "false":
glob_list = []
for case in cases:
clusters_db = case.to_json()["clusters"]
loc_cluster = [cluster["name"] for cluster in clusters_db]
galaxies_list = [Galaxy.query.get(cluster["galaxy_id"]).name for cluster in clusters_db]
if (not clusters or all(item in loc_cluster for item in clusters)) and \
(not galaxies or all(item in galaxies_list for item in galaxies)):
glob_list.append(case)
cases = glob_list
else:
cases = Case.query.filter_by(completed=completed).order_by(desc(filter)).paginate(page=page, per_page=25, max_per_page=50)
nb_pages = cases.pages
# for deadline filter, only case with a deadline defined is required
loc = list()
for case in cases:
if getattr(case, filter):
loc.append(case)
return loc, nb_pages
def fork_case_core(cid, case_title_fork, user):
case_title_stored = CommonModel.get_case_by_title(case_title_fork)
if case_title_stored:
return {"message": "Error, title already exist"}
case = CommonModel.get_case(cid)
case_json = case.to_json()
case_json["title"] = case_title_fork
if case.deadline:
case_json["deadline_date"] = datetime.datetime.strptime(case_json["deadline"].split(" ")[0], "%Y-%m-%d").date()
case_json["deadline_time"] = datetime.datetime.strptime(case_json["deadline"].split(" ")[1], "%H:%M").time()
else:
case_json["deadline_date"] = None
case_json["deadline_time"] = None
new_case = create_case(case_json, user)
for task in case.tasks:
task_json = task.to_json()
if task.deadline:
task_json["deadline_date"] = datetime.datetime.strptime(task_json["deadline"].split(" ")[0], "%Y-%m-%d").date()
task_json["deadline_time"] = datetime.datetime.strptime(task_json["deadline"].split(" ")[1], "%H:%M").time()
else:
task_json["deadline_date"] = None
task_json["deadline_time"] = None
TaskModel.create_task(task_json, new_case.id, user)
return new_case
def create_template_from_case(cid, case_title_template):
if Case_Template.query.filter_by(title=case_title_template).first():
return {"message": "Error, title already exist"}
case = CommonModel.get_case(cid)
new_template = Case_Template(
uuid=str(uuid.uuid4()),
title=case_title_template,
description=case.description
)
db.session.add(new_template)
db.session.commit()
for c_t in Case_Tags.query.filter_by(case_id=case.id).all():
case_tag = Case_Template_Tags(
case_id=new_template.id,
tag_id=c_t.tag_id
)
db.session.add(case_tag)
db.session.commit()
for c_t in Case_Galaxy_Tags.query.filter_by(case_id=case.id).all():
case_cluster = Case_Template_Galaxy_Tags(
template_id=new_template.id,
cluster_id=c_t.cluster_id
)
db.session.add(case_cluster)
db.session.commit()
for task in case.tasks:
task_exist = Task_Template.query.filter_by(title=task.title).first()
if not task_exist:
task_template = Task_Template(
uuid=str(uuid.uuid4()),
title=task.title,
description=task.description,
url=task.url
)
db.session.add(task_template)
db.session.commit()
## Tags
for t_t in Task_Tags.query.filter_by(task_id=task.id).all():
task_tag = Task_Template_Tags(
task_id=task_template.id,
tag_id=t_t.tag_id
)
db.session.add(task_tag)
db.session.commit()
## Clusters
for t_t in Task_Galaxy_Tags.query.filter_by(task_id=task.id).all():
task_cluster = Task_Template_Galaxy_Tags(
template_id=task_template.id,
cluster_id=t_t.cluster_id
)
db.session.add(task_cluster)
db.session.commit()
## Task Connectors
for t_c in Task_Connector_Instance.query.filter_by(task_id=task.id).all():
task_connector = Task_Template_Connector_Instance(
template_id=task_template.id,
instance_id=t_c.instance_id
)
db.session.add(task_connector)
db.session.commit()
case_task_template = Case_Task_Template(
case_id=new_template.id,
task_id=task_template.id
)
db.session.add(case_task_template)
db.session.commit()
else:
case_task_template = Case_Task_Template(
case_id=new_template.id,
task_id=task_exist.id
)
db.session.add(case_task_template)
db.session.commit()
return new_template
def change_recurring(form_dict, cid, current_user):
case = CommonModel.get_case(cid)
recurring_status = Status.query.filter_by(name="Recurring").first()
created_status = Status.query.filter_by(name="Created").first()
if "once" in form_dict and form_dict["once"]:
case.recurring_type = "once"
case.recurring_date = form_dict["once"]
case.status_id = recurring_status.id
elif "daily" in form_dict and form_dict["daily"]:
case.recurring_type = "daily"
case.recurring_date = datetime.datetime.today() + datetime.timedelta(days=1)
case.status_id = recurring_status.id
elif "weekly" in form_dict and form_dict["weekly"]:
case.recurring_type = "weekly"
if form_dict["weekly"]<datetime.datetime.today().date():
case.recurring_date = datetime.datetime.today() + datetime.timedelta(
days=(form_dict["weekly"].weekday() - datetime.datetime.today().weekday() + 7)
)
else:
case.recurring_date = form_dict["weekly"]
case.status_id = recurring_status.id
elif "monthly" in form_dict and form_dict["monthly"]:
case.recurring_type = "monthly"
if form_dict["monthly"]<datetime.datetime.today().date():
case.recurring_date = form_dict["monthly"] + relativedelta.relativedelta(months=1)
else:
case.recurring_date = form_dict["monthly"]
case.status_id = recurring_status.id
elif "remove" in form_dict and form_dict["remove"]:
case.recurring_type = None
case.recurring_date = None
case.status_id = created_status.id
for notif in Recurring_Notification.query.filter_by(case_id=cid).all():
db.session.delete(notif)
db.session.commit()
else:
return False
db.session.commit()
CommonModel.save_history(case.uuid, current_user, "Recurring changed")
return True
def notify_user(task, user_id):
case = CommonModel.get_case(task.case_id)
message = f"Notify for task '{task.id}-{task.title}' of case '{case.id}-{case.title}'"
NotifModel.create_notification_user(message, task.case_id, user_id=user_id, html_icon="fa-solid fa-bell")
return True
def notify_user_recurring(form_dict, case_id, orgs):
for org in orgs:
if f"check_{org.id}" in form_dict:
for user in org.users:
if not Recurring_Notification.query.filter_by(case_id=case_id, user_id=user.id).first():
rec_notif = Recurring_Notification(case_id=case_id, user_id=user.id)
db.session.add(rec_notif)
db.session.commit()
else:
for user in org.users:
if f"check_{org.id}_user_{user.id}" in form_dict:
if not Recurring_Notification.query.filter_by(case_id=case_id, user_id=user.id).first():
rec_notif = Recurring_Notification(case_id=case_id, user_id=user.id)
db.session.add(rec_notif)
db.session.commit()
else:
notif = Recurring_Notification.query.filter_by(case_id=case_id, user_id=user.id).first()
if notif:
db.session.delete(notif)
db.session.commit()
def get_modules():
return MODULES_CONFIG
def get_instance_module_core(module, user_id):
connector = CommonModel.get_connector_by_name(MODULES_CONFIG[module]["config"]["connector"])
instance_list = list()
for instance in connector.instances:
if CommonModel.get_user_instance_both(user_id=user_id, instance_id=instance.id):
instance_list.append(instance.to_json())
return instance_list

View File

@ -0,0 +1,160 @@
from ..db_class.db import Case, User
from datetime import datetime
from . import common_core as CommonModel
from ..utils.utils import check_tag
def get_user_api(api_key):
return User.query.filter_by(api_key=api_key).first()
def verif_set_recurring(data_dict):
if "once" in data_dict:
try:
data_dict["once"] = datetime.strptime(data_dict["once"], '%Y-%m-%d')
except:
return {"message": "once date bad format, YYYY-mm-dd"}
if "weekly" in data_dict:
try:
data_dict["weekly"] = datetime.strptime(data_dict["weekly"], '%Y-%m-%d').date()
except:
return {"message": "weekly date bad format, YYYY-mm-dd"}
if "monthly" in data_dict:
try:
data_dict["monthly"] = datetime.strptime(data_dict["monthly"], '%Y-%m-%d').date()
except:
return {"message": "monthly date bad format, YYYY-mm-dd"}
return data_dict
def verif_create_case_task(data_dict, isCase):
if "title" not in data_dict or not data_dict["title"]:
return {"message": "Please give a title"}
elif Case.query.filter_by(title=data_dict["title"]).first():
return {"message": "Title already exist"}
if "description" not in data_dict or not data_dict["description"]:
data_dict["description"] = ""
if "deadline_date" in data_dict:
try:
data_dict["deadline_date"] = datetime.strptime(data_dict["deadline_date"], '%Y-%m-%d')
except:
return {"message": "deadline_date bad format"}
else:
data_dict["deadline_date"] = ""
if "deadline_time" in data_dict:
try:
data_dict["deadline_time"] = datetime.strptime(data_dict["deadline_time"], '%H-%M')
except:
return {"message": "deadline_time bad format"}
else:
data_dict["deadline_time"] = ""
if "tags" in data_dict:
for tag in data_dict["tags"]:
if not check_tag(tag):
return {"message": f"Tag '{tag}' doesn't exist"}
else:
data_dict["tags"] = []
if "clusters" in data_dict:
for cluster in data_dict["clusters"]:
if not CommonModel.check_cluster(cluster):
return {"message": f"Cluster '{cluster}' doesn't exist"}
else:
data_dict["clusters"] = []
if not isCase:
if "url" not in data_dict or not data_dict["url"]:
data_dict["url"] = ""
if "connectors" in data_dict:
for connector in data_dict["connectors"]:
if not CommonModel.check_connector(connector):
return {"message": f"Connector '{connector}' doesn't exist"}
else:
data_dict["connectors"] = []
return data_dict
def common_verif(data_dict, case_task):
if "description" not in data_dict or not data_dict["description"]:
data_dict["description"] = case_task.description
if "deadline_date" in data_dict:
try:
data_dict["deadline_date"] = datetime.strptime(data_dict["deadline_date"], '%Y-%m-%d')
except:
return {"message": "date bad format"}
elif case_task.deadline:
data_dict["deadline_date"] = case_task.deadline.strftime('%Y-%m-%d')
else:
data_dict["deadline_date"] = ""
if "deadline_time" in data_dict:
try:
data_dict["deadline_time"] = datetime.strptime(data_dict["deadline_time"], '%H-%M')
except:
return {"message": "time bad format"}
elif case_task.deadline:
data_dict["deadline_time"] = case_task.deadline.strftime('%H-%M')
else:
data_dict["deadline_time"] = ""
if "tags" in data_dict:
for tag in data_dict["tags"]:
if not check_tag(tag):
return {"message": f"Tag '{tag}' doesn't exist"}
elif case_task.to_json()["tags"]:
data_dict["tags"] = case_task.to_json()["tags"]
else:
data_dict["tags"] = []
if "clusters" in data_dict:
for cluster in data_dict["clusters"]:
if not CommonModel.check_cluster(cluster):
return {"message": f"Cluster '{cluster}' doesn't exist"}
elif case_task.to_json()["clusters"]:
data_dict["clusters"] = case_task.to_json()["clusters"]
else:
data_dict["clusters"] = []
return data_dict
def verif_edit_case(data_dict, case_id):
case = CommonModel.get_case(case_id)
if "title" not in data_dict or data_dict["title"] == case.title or not data_dict["title"]:
data_dict["title"] = case.title
elif Case.query.filter_by(title=data_dict["title"]).first():
return {"message": "Title already exist"}
data_dict = common_verif(data_dict, case)
return data_dict
def verif_edit_task(data_dict, task_id):
task = CommonModel.get_task(task_id)
if "title" not in data_dict or data_dict["title"] == task.title or not data_dict["title"]:
data_dict["title"] = task.title
data_dict = common_verif(data_dict, task)
if "url" not in data_dict or not data_dict["url"]:
data_dict["url"] = task.url
if "connectors" in data_dict:
for connector in data_dict["connectors"]:
if not CommonModel.check_connector(connector):
return {"message": f"connector '{connector}' doesn't exist"}
elif task.to_json()["connectors"]:
data_dict["connectors"] = task.to_json()["connectors"]
else:
data_dict["connectors"] = []
return data_dict

View File

@ -0,0 +1,251 @@
import os
import shutil
import datetime
from flask import flash
from .. import db
from ..db_class.db import *
from ..utils.utils import isUUID, create_specific_dir
from sqlalchemy import desc, func
from ..utils import utils
UPLOAD_FOLDER = os.path.join(os.getcwd(), "uploads")
TEMP_FOLDER = os.path.join(os.getcwd(), "temp")
HISTORY_FOLDER = os.path.join(os.getcwd(), "history")
def get_case(cid):
"""Return a case by is id"""
if isUUID(cid):
case = Case.query.filter_by(uuid=cid).first()
elif str(cid).isdigit():
case = Case.query.get(cid)
else:
case = None
return case
def get_task(tid):
"""Return a task by is id"""
if isUUID(tid):
case = Task.query.filter_by(uuid=tid).first()
elif str(tid).isdigit():
case = Task.query.get(tid)
else:
case = None
return case
def get_all_cases():
"""Return all cases"""
return Case.query.filter_by(completed=False).order_by(desc(Case.last_modif))
def get_case_by_completed(completed):
return Case.query.filter_by(completed=completed)
def get_case_by_title(title):
return Case.query.where(func.lower(Case.title)==func.lower(title)).first()
def get_case_template_by_title(title):
return Case_Template.query.filter_by(title=title).first()
def get_task_templates():
return Task_Template.query.all()
def search(text):
"""Return cases containing text"""
return Case.query.where(Case.title.contains(text), Case.completed==False).paginate(page=1, per_page=30, max_per_page=50)
def get_all_users_core(case):
return Org.query.join(Case_Org, Case_Org.case_id==case.id).where(Case_Org.org_id==Org.id).all()
def get_role(user):
"""Return role for the current user"""
return Role.query.get(user.role_id)
def get_org(oid):
"""Return an org by is id"""
return Org.query.get(oid)
def get_org_by_name(name):
"""Return an org by is name"""
return Org.query.filter_by(name=name).first()
def get_org_order_by_name():
"""Return all orgs order by name"""
return Org.query.order_by("name")
def get_org_in_case(org_id, case_id):
return Case_Org.query.filter_by(case_id=case_id, org_id=org_id).first()
def get_org_in_case_by_case_id(case_id):
return Case_Org.query.filter_by(case_id=case_id).all()
def get_orgs_in_case(case_id):
"""Return orgs present in a case"""
case_org = Case_Org.query.filter_by(case_id=case_id).all()
return [Org.query.get(c_o.org_id) for c_o in case_org ]
def get_file(fid):
return File.query.get(fid)
def get_all_status():
return Status.query.all()
def get_status(sid):
return Status.query.get(sid).first()
def get_recu_notif_user(case_id, user_id):
return Recurring_Notification.query.filter_by(case_id=case_id, user_id=user_id).first()
def get_taxonomies():
return [taxo.to_json() for taxo in Taxonomy.query.filter_by(exclude=False).all()]
def get_galaxy(galaxy_id):
return Galaxy.query.get(galaxy_id)
def get_galaxies():
return [galax.to_json() for galax in Galaxy.query.filter_by(exclude=False).order_by('name').all()]
def get_clusters():
return [cluster.to_json() for cluster in Cluster.query.all()]
def get_cluster_by_name(cluster):
return Cluster.query.filter_by(name=cluster).first()
def get_clusters_galaxy(galaxies):
out = dict()
for galaxy in galaxies:
out[galaxy] = [cluster.to_json() for cluster in Cluster.query.join(Galaxy, Galaxy.id==Cluster.galaxy_id).where(Galaxy.name==galaxy).all() if not cluster.exclude]
return out
def get_tags(taxos):
out = dict()
for taxo in taxos:
out[taxo] = [tag.to_json() for tag in Taxonomy.query.filter_by(name=taxo).first().tags if not tag.exclude]
return out
def get_tag(tag):
return Tags.query.filter_by(name=tag).first()
def get_case_tags(cid):
return [tag.name for tag in Tags.query.join(Case_Tags, Case_Tags.tag_id==Tags.id).filter_by(case_id=cid).all()]
def get_case_clusters(cid):
return [cluster for cluster in Cluster.query.join(Case_Galaxy_Tags, Case_Galaxy_Tags.cluster_id==Cluster.id).filter_by(case_id=cid).all()]
def get_task_tags(tid):
return [tag.name for tag in Tags.query.join(Task_Tags, Task_Tags.tag_id==Tags.id).filter_by(task_id=tid).all()]
def get_task_clusters(tid):
return [cluster for cluster in Cluster.query.join(Task_Galaxy_Tags, Task_Galaxy_Tags.cluster_id==Cluster.id).filter_by(task_id=tid).all()]
def get_connectors():
return Connector.query.all()
def get_connector(cid):
return Connector.query.get(cid)
def get_connector_by_name(name):
return Connector.query.where(Connector.name.like(name)).first()
def get_instance(iid):
return Connector_Instance.query.get(iid)
def get_instance_by_name(name):
return Connector_Instance.query.filter_by(name=name).first()
def get_task_connectors(tid):
return Task_Connector_Instance.query.filter_by(task_id=tid).all()
def get_user_instance_both(user_id, instance_id):
return User_Connector_Instance.query.filter_by(user_id=user_id, instance_id=instance_id).all()
def get_user_instance_by_instance(instance_id):
"""Return a user instance by instance id"""
return User_Connector_Instance.query.filter_by(instance_id=instance_id).first()
def get_history(case_uuid):
try:
path_history = os.path.join(HISTORY_FOLDER, str(case_uuid))
with open(path_history, "r") as read_file:
loc_file = read_file.read().splitlines()
return loc_file
except:
return False
def save_history(case_uuid, current_user, message):
create_specific_dir(HISTORY_FOLDER)
path_history = os.path.join(HISTORY_FOLDER, str(case_uuid))
with open(path_history, "a") as write_history:
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
write_history.write(f"[{now}]({current_user.first_name} {current_user.last_name}): {message}\n")
def update_last_modif(case_id):
"""Update 'last_modif' of a case"""
case = Case.query.get(case_id)
case.last_modif = datetime.datetime.now(tz=datetime.timezone.utc)
db.session.commit()
def update_last_modif_task(task_id):
"""Update 'last_modif' of a task"""
if task_id:
task = Task.query.get(task_id)
task.last_modif = datetime.datetime.now(tz=datetime.timezone.utc)
db.session.commit()
def deadline_check(date, time):
"""Combine the date and the time if time exist"""
deadline = None
if date and time:
deadline = datetime.datetime.combine(date, time)
elif date:
deadline = date
return deadline
def delete_temp_folder():
shutil.rmtree(TEMP_FOLDER)
def check_cluster_db(cluster):
return Cluster.query.filter_by(name=cluster).first()
def check_tag(tag_list):
flag = True
for tag in tag_list:
if not utils.check_tag(tag):
flag = False
if not flag:
flash("tag doesn't exist")
return flag
def check_cluster(cluster_list):
flag = True
for cluster in cluster_list:
if not check_cluster_db(cluster):
flag = False
if not flag:
flash("cluster doesn't exist")
return flag
def check_connector(connector_list):
flag = True
for connector in connector_list:
if not get_instance_by_name(connector):
flag = False
if not flag:
flash("Connector doesn't exist")
return flag

116
webiste/app/case/form.py Normal file
View File

@ -0,0 +1,116 @@
from flask_wtf import FlaskForm
from wtforms import ValidationError
from wtforms.fields import (
StringField,
SubmitField,
SelectMultipleField,
TextAreaField,
DateField,
TimeField,
HiddenField,
BooleanField
)
from wtforms.validators import InputRequired, Length, Optional
from ..db_class.db import Case, Case_Org
class CaseForm(FlaskForm):
title = StringField('Title', validators=[Optional(), Length(1, 64)])
description = TextAreaField('Description', validators=[Optional()])
deadline_date = DateField('deadline_date', validators=[Optional()])
deadline_time = TimeField("deadline_time", validators=[Optional()])
template_select = SelectMultipleField(u'Templates', coerce=int)
title_template = StringField('Title Template', validators=[Optional(), Length(1, 64)])
tasks_templates = SelectMultipleField(u'Tasks Templates', coerce=int)
submit = SubmitField('Create')
def validate_title(self, field):
if not field.data and 0 in self.template_select.data:
raise ValidationError("Need to select a title or a template")
if Case.query.filter_by(title=field.data).first():
raise ValidationError("The title already exist")
def validate_template_select(self, field):
if 0 in field.data and not self.title.data:
raise ValidationError("Need to select a template or a title")
if not 0 in field.data and not self.title_template.data:
raise ValidationError("Need a title for the case")
def validate_deadline_time(self, field):
if field.data and not self.deadline_date.data:
raise ValidationError("Choose a date")
def validate_title_template(self, field):
if field.data and not self.template_select.data or 0 in self.template_select.data:
raise ValidationError("A template need to be selected")
if Case.query.filter_by(title=field.data).first():
raise ValidationError("The title already exist")
class CaseEditForm(FlaskForm):
title = StringField('Title', validators=[InputRequired(), Length(1, 64)])
description = TextAreaField('Description', validators=[Optional()])
deadline_date = DateField('deadline_date', validators=[Optional()])
deadline_time = TimeField("deadline_time", validators=[Optional()])
submit = SubmitField('Save')
def validate_deadline_time(self, field):
if field.data and not self.deadline_date.data:
raise ValidationError("Choose a date")
class TaskForm(FlaskForm):
title = StringField('Title', validators=[Optional(), Length(1, 64)])
description = TextAreaField('Description', validators=[Optional()])
url = StringField('Tool/Link', validators=[Optional(), Length(0, 64)])
deadline_date = DateField('deadline_date', validators=[Optional()])
deadline_time = TimeField("deadline_time", validators=[Optional()])
template_select = SelectMultipleField(u'Templates', coerce=int)
submit = SubmitField('Create')
def validate_title(self, field):
if not field.data and 0 in self.template_select.data:
raise ValidationError("Need to select a title or a template")
def validate_template_select(self, field):
if 0 in field.data and not self.title.data:
raise ValidationError("Need to select a template or a title")
def validate_deadline_time(self, field):
if field.data and not self.deadline_date.data:
raise ValidationError("Choose a date")
class TaskEditForm(FlaskForm):
title = StringField('Title', validators=[InputRequired(), Length(1, 64)])
description = TextAreaField('Description', validators=[Optional()])
url = StringField('Tool/Link', validators=[Optional(), Length(0, 64)])
deadline_date = DateField('deadline_date', validators=[Optional()])
deadline_time = TimeField("deadline_time", validators=[Optional()])
submit = SubmitField('Save')
def validate_deadline_time(self, field):
if field.data and not self.deadline_date.data:
raise ValidationError("Choose a date")
class AddOrgsCase(FlaskForm):
org_id = SelectMultipleField(u'Orgs', coerce=int)
case_id = HiddenField("")
submit = SubmitField('Add')
def validate_org_id(self, field):
for org in field.data:
if Case_Org.query.filter_by(case_id = self.case_id.data, org_id=org).first():
raise ValidationError(f"Org {org} already in case")
class RecurringForm(FlaskForm):
case_id = HiddenField("")
once = DateField('One day', validators=[Optional()])
daily = BooleanField('Daily', validators=[Optional()])
weekly = DateField("Start date", validators=[Optional()])
monthly = DateField("Start date", validators=[Optional()])
remove = BooleanField('Remove', validators=[Optional()])
submit = SubmitField('Save')

464
webiste/app/case/task.py Normal file
View File

@ -0,0 +1,464 @@
from flask import Blueprint, render_template, redirect, jsonify, request, flash
from .form import TaskEditForm, TaskForm
from flask_login import login_required, current_user
from . import case_core as CaseModel
from . import common_core as CommonModel
from . import task_core as TaskModel
from ..decorators import editor_required
from ..utils.utils import form_to_dict
task_blueprint = Blueprint(
'task',
__name__,
template_folder='templates',
static_folder='static'
)
@task_blueprint.route("/<cid>/create_task", methods=['GET', 'POST'])
@login_required
def create_task(cid):
"""View of a case"""
if CommonModel.get_case(cid):
present_in_case = CaseModel.get_present_in_case(cid, current_user)
if present_in_case or current_user.is_admin():
form = TaskForm()
form.template_select.choices = [(template.id, template.title) for template in CommonModel.get_task_templates()]
form.template_select.choices.insert(0, (0," "))
if form.validate_on_submit():
tag_list = request.form.getlist("tags_select")
cluster_list = request.form.getlist("clusters_select")
connector_list = request.form.getlist("connectors_select")
if CommonModel.check_tag(tag_list):
if CommonModel.check_cluster(cluster_list):
form_dict = form_to_dict(form)
form_dict["tags"] = tag_list
form_dict["clusters"] = cluster_list
form_dict["connectors"] = connector_list
if TaskModel.create_task(form_dict, cid, current_user):
flash("Task created", "success")
else:
flash("Error Task Created", "error")
return redirect(f"/case/{cid}")
return render_template("case/create_task.html", form=form)
return render_template("case/create_task.html", form=form)
return render_template("case/create_task.html", form=form)
return redirect(f"/case/{cid}")
return render_template("404.html")
@task_blueprint.route("/<cid>/edit_task/<tid>", methods=['GET','POST'])
@login_required
@editor_required
def edit_task(cid, tid):
"""Edit the task"""
if CommonModel.get_case(cid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
form = TaskEditForm()
if form.validate_on_submit():
tag_list = request.form.getlist("tags_select")
cluster_list = request.form.getlist("clusters_select")
connector_list = request.form.getlist("connectors_select")
if CommonModel.check_tag(tag_list):
if CommonModel.check_cluster(cluster_list):
form_dict = form_to_dict(form)
form_dict["tags"] = tag_list
form_dict["clusters"] = cluster_list
form_dict["connectors"] = connector_list
TaskModel.edit_task_core(form_dict, tid, current_user)
flash("Task edited", "success")
return redirect(f"/case/{cid}")
return render_template("case/create_task.html", form=form)
return render_template("case/create_task.html", form=form)
else:
task_modif = CommonModel.get_task(tid)
form.description.data = task_modif.description
form.title.data = task_modif.title
form.url.data = task_modif.url
form.deadline_date.data = task_modif.deadline
form.deadline_time.data = task_modif.deadline
return render_template("case/edit_task.html", form=form)
else:
flash("Access denied", "error")
return redirect(f"/case/{cid}")
return render_template("404.html")
@task_blueprint.route("/complete_task/<tid>", methods=['GET'])
@login_required
@editor_required
def complete_task(tid):
"""Complete the task"""
task = CommonModel.get_task(str(tid))
if task:
if CaseModel.get_present_in_case(task.case_id, current_user) or current_user.is_admin():
if TaskModel.complete_task(tid, current_user):
return {"message": "Task completed", "toast_class": "success-subtle"}, 200
return {"message": "Error task completed", "toast_class": "danger-subtle"}, 400
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "Task not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/<cid>/delete_task/<tid>", methods=['GET'])
@login_required
@editor_required
def delete_task(cid, tid):
"""Delete the task"""
if CommonModel.get_case(cid):
if CommonModel.get_task(tid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if TaskModel.delete_task(tid, current_user):
return {"message": "Task deleted", "toast_class": "success-subtle"}, 200
return {"message": "Error task deleted", "toast_class": "danger-subtle"}, 400
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "Task not found", "toast_class": "danger-subtle"}, 404
return {"message": "Case not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/<cid>/modif_note/<tid>", methods=['POST'])
@login_required
@editor_required
def modif_note(cid, tid):
"""Modify note of the task"""
if CommonModel.get_case(cid):
if CommonModel.get_task(tid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
notes = request.json["notes"]
if TaskModel.modif_note_core(tid, current_user, notes):
return {"message": "Note added", "toast_class": "success-subtle"}, 200
return {"message": "Error add/modify note", "toast_class": "danger-subtle"}, 400
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "Task not found", "toast_class": "danger-subtle"}, 404
return {"message": "Case not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/<cid>/get_note/<tid>", methods=['GET'])
@editor_required
def get_note(cid, tid):
"""Get not of a task in text format"""
if CommonModel.get_case(cid):
task = CommonModel.get_task(tid)
if task:
return {"note": task.notes}, 201
return {"message": "Task not found", "toast_class": "danger-subtle"}, 404
return {"message": "Case not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/<cid>/take_task/<tid>", methods=['GET'])
@login_required
@editor_required
def take_task(cid, tid):
"""Assign current user to the task"""
if CommonModel.get_case(cid):
if CommonModel.get_task(tid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if TaskModel.assign_task(tid, user=current_user, current_user=current_user, flag_current_user=True):
return {"message": "User Assigned", "toast_class": "success-subtle"}, 200
return {"message": "Error assignment", "toast_class": "danger-subtle"}, 400
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "Task not found", "toast_class": "danger-subtle"}, 404
return {"message": "Case not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/<cid>/assign_users/<tid>", methods=['POST'])
@login_required
@editor_required
def assign_user(cid, tid):
"""Assign a list of users to the task"""
if CommonModel.get_case(cid):
if "users_id" in request.json:
users_list = request.json["users_id"]
if CommonModel.get_task(tid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
for user in users_list:
TaskModel.assign_task(tid, user=user, current_user=current_user, flag_current_user=False)
return {"message": "Users Assigned", "toast_class": "success-subtle"}, 200
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "Task not found", "toast_class": "danger-subtle"}, 404
return {"message": "'users_id' is missing", "toast_class": "danger-subtle"}, 400
return {"message": "Case not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/<cid>/remove_assignment/<tid>", methods=['GET'])
@login_required
@editor_required
def remove_assign_task(cid, tid):
"""Remove current user assignment to the task"""
if CommonModel.get_case(cid):
if CommonModel.get_task(tid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if TaskModel.remove_assign_task(tid, user=current_user, current_user=current_user, flag_current_user=True):
return {"message": "User Removed from assignment", "toast_class": "success-subtle"}, 200
return {"message": "Error removed assignment", "toast_class": "danger-subtle"}, 400
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "Task not found", "toast_class": "danger-subtle"}, 404
return {"message": "Case not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/<cid>/remove_assigned_user/<tid>", methods=['POST'])
@login_required
@editor_required
def remove_assigned_user(cid, tid):
"""Assign current user to the task"""
if CommonModel.get_case(cid):
if "user_id" in request.json:
user_id = request.json["user_id"]
if CommonModel.get_task(tid):
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if TaskModel.remove_assign_task(tid, user=user_id, current_user=current_user, flag_current_user=False):
return {"message": "User Removed from assignment", "toast_class": "success-subtle"}, 200
return {"message": "Error removed assignment", "toast_class": "danger-subtle"}, 400
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "Task not found", "toast_class": "danger-subtle"}, 404
return {"message": "'user_id' is missing", "toast_class": "danger-subtle"}, 400
return {"message": "Case not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/<cid>/change_task_status/<tid>", methods=['POST'])
@login_required
@editor_required
def change_task_status(cid, tid):
"""Change the status of the task"""
if CommonModel.get_case(cid):
if "status" in request.json:
status = request.json["status"]
task = CommonModel.get_task(tid)
if task:
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if TaskModel.change_task_status(status, task, current_user):
return {"message": "Status changed", "toast_class": "success-subtle"}, 200
return {"message": "Error changed status", "toast_class": "danger-subtle"}, 400
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "Task not found", "toast_class": "danger-subtle"}, 404
return {"message": "'status' is missing", "toast_class": "danger-subtle"}, 400
return {"message": "Case not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/task/<tid>/download_file/<fid>", methods=['GET'])
@login_required
@editor_required
def download_file(tid, fid):
"""Download the file"""
task = CommonModel.get_task(tid)
file = CommonModel.get_file(fid)
if file and file in task.files:
if CaseModel.get_present_in_case(task.case_id, current_user) or current_user.is_admin():
return TaskModel.download_file(file)
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "File not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/task/<tid>/delete_file/<fid>", methods=['GET'])
@login_required
@editor_required
def delete_file(tid, fid):
"""Delete the file"""
task = CommonModel.get_task(tid)
file = CommonModel.get_file(fid)
if file and file in task.files:
if CaseModel.get_present_in_case(task.case_id, current_user) or current_user.is_admin():
if TaskModel.delete_file(file, task, current_user):
return {"message": "File Deleted", "toast_class": "success-subtle"}, 200
return {"message": "Error deleting file"}, 400
return {"message": "Action not allowed", "toast_class": "warning-subtle"}, 401
return {"message": "File not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/<cid>/add_files/<tid>", methods=['POST'])
@login_required
@editor_required
def add_files(cid, tid):
"""Add files to a task"""
if CommonModel.get_case(cid):
task = CommonModel.get_task(tid)
if task:
if len(request.files) > 0:
if TaskModel.add_file_core(task=task, files_list=request.files, current_user=current_user):
return {"message":"Files added", "toast_class": "success-subtle"}, 200
return {"message":"Something goes wrong adding files", "toast_class": "danger-subtle"}, 400
return {"message":"No Files given", "toast_class": "warning-subtle"}, 400
return {"message":"Task not found", "toast_class": "danger-subtle"}, 404
return {"message":"Case not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/<cid>/get_files/<tid>", methods=['GET'])
@login_required
@editor_required
def get_files(cid, tid):
"""Get files of a task"""
if CommonModel.get_case(cid):
task = CommonModel.get_task(tid)
if task:
file_list = [file.to_json() for file in task.files]
return {"files": file_list}, 200
return {"message":"Task not found", "toast_class": "danger-subtle"}, 404
return {"message":"Case not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/<cid>/sort_by_ongoing_task", methods=['GET'])
@login_required
def sort_by_ongoing_task(cid):
"""Sort Task by living one"""
case = CommonModel.get_case(cid)
tags = request.args.get('tags')
or_and_taxo = request.args.get("or_and_taxo")
taxonomies = request.args.get('taxonomies')
galaxies = request.args.get('galaxies')
clusters = request.args.get('clusters')
or_and_galaxies = request.args.get("or_and_galaxies")
return TaskModel.sort_by_status_task_core(case, current_user, taxonomies, galaxies, tags, clusters, or_and_taxo, or_and_galaxies, completed=False)
@task_blueprint.route("/<cid>/sort_by_finished_task", methods=['GET'])
@login_required
def sort_by_finished_task(cid):
"""Sort task by finished one"""
case = CommonModel.get_case(cid)
tags = request.args.get('tags')
or_and_taxo = request.args.get("or_and_taxo")
taxonomies = request.args.get('taxonomies')
galaxies = request.args.get('galaxies')
clusters = request.args.get('clusters')
or_and_galaxies = request.args.get("or_and_galaxies")
return TaskModel.sort_by_status_task_core(case, current_user, taxonomies, galaxies, tags, clusters, or_and_taxo, or_and_galaxies, completed=True)
@task_blueprint.route("/<cid>/tasks/ongoing", methods=['GET'])
@login_required
def ongoing_tasks_sort_by_filter(cid):
"""Sort by filter for living task"""
tags = request.args.get('tags')
or_and_taxo = request.args.get("or_and_taxo")
taxonomies = request.args.get('taxonomies')
galaxies = request.args.get('galaxies')
clusters = request.args.get('clusters')
or_and_galaxies = request.args.get("or_and_galaxies")
filter = request.args.get('filter')
if filter:
case = CommonModel.get_case(cid)
return TaskModel.sort_tasks_by_filter(case, current_user, filter, taxonomies, galaxies, tags, clusters, or_and_taxo, or_and_galaxies, completed=False)
return {"message": "No filter pass"}, 400
@task_blueprint.route("/<cid>/tasks/finished", methods=['GET'])
@login_required
def finished_tasks_sort_by_filter(cid):
"""Sort by filter for finished task"""
tags = request.args.get('tags')
or_and_taxo = request.args.get("or_and_taxo")
taxonomies = request.args.get('taxonomies')
filter = request.args.get('filter')
galaxies = request.args.get('galaxies')
clusters = request.args.get('clusters')
or_and_galaxies = request.args.get("or_and_galaxies")
if filter:
case = CommonModel.get_case(cid)
return TaskModel.sort_tasks_by_filter(case, current_user, filter, taxonomies, galaxies, tags, clusters, or_and_taxo, or_and_galaxies, completed=True)
return {"message": "No filter pass"}, 400
@task_blueprint.route("/<cid>/task/<tid>/notify_user", methods=['POST'])
@login_required
@editor_required
def notify_user(cid, tid):
"""Notify a user about a task"""
if CommonModel.get_case(cid):
if "user_id" in request.json:
user = request.json["user_id"]
task = CommonModel.get_task(tid)
if task:
if CaseModel.get_present_in_case(cid, current_user) or current_user.is_admin():
if CaseModel.notify_user(task, user):
return {"message":"User notified", "toast_class": "success-subtle"}, 200
return {"message":"Something goes wrong", "toast_class": "danger-subtle"}, 400
return {"message":"Action not Allowed", "toast_class": "warning-subtle"}, 400
return {"message":"Task not found", "toast_class": "danger-subtle"}, 404
return {"message": "'user_id' is missing", "toast_class": "danger-subtle"}, 404
return {"message": "Case not found", "toast_class": "danger-subtle"}, 404
@task_blueprint.route("/<cid>/task/<tid>/export_notes", methods=['GET'])
@login_required
def export_notes(cid, tid):
"""Export note of a task as pdf"""
case = CommonModel.get_case(cid)
if case:
task = CommonModel.get_task(tid)
if task:
data_dict = dict(request.args)
if "type" in data_dict:
type_req = data_dict["type"]
res = TaskModel.export_notes(task, type_req)
CommonModel.delete_temp_folder()
return res
return {"message": "'type' is missing", 'toast_class': "warning-subtle"}, 400
return {"message": "Task not found", 'toast_class': "danger-subtle"}, 404
return {"message": "Case not found", 'toast_class': "danger-subtle"}, 404
@task_blueprint.route("/get_taxonomies_task/<tid>", methods=['GET'])
@login_required
def get_taxonomies_case(tid):
task = CommonModel.get_task(tid)
if task:
tags = CommonModel.get_task_tags(task.id)
taxonomies = []
if tags:
taxonomies = [tag.split(":")[0] for tag in tags]
return {"tags": tags, "taxonomies": taxonomies}
return {"message": "task Not found", 'toast_class': "danger-subtle"}, 404
@task_blueprint.route("/get_galaxies_task/<tid>", methods=['GET'])
@login_required
def get_galaxies_task(tid):
task = CommonModel.get_task(tid)
if task:
clusters = CommonModel.get_task_clusters(task.id)
galaxies = []
if clusters:
for cluster in clusters:
loc_g = CommonModel.get_galaxy(cluster.galaxy_id)
if not loc_g.name in galaxies:
galaxies.append(loc_g.name)
index = clusters.index(cluster)
clusters[index] = cluster.tag
return {"clusters": clusters, "galaxies": galaxies}
return {"message": "task Not found", 'toast_class': "danger-subtle"}, 404
@task_blueprint.route("/get_connectors", methods=['GET'])
@login_required
def get_connectors():
connectors_list = CommonModel.get_connectors()
connectors_dict = dict()
for connector in connectors_list:
loc = list()
for instance in connector.instances:
if CommonModel.get_user_instance_both(user_id=current_user.id, instance_id=instance.id):
loc.append(instance.to_json())
if loc:
connectors_dict[connector.name] = loc
return jsonify({"connectors": connectors_dict}), 200
@task_blueprint.route("/get_connectors_task/<tid>", methods=['GET'])
@login_required
def get_connectors_task(tid):
task = CommonModel.get_task(tid)
if task:
return {"connectors": [CommonModel.get_instance(task_instance.instance_id).name for task_instance in CommonModel.get_task_connectors(task.id) ]}
return {"message": "task Not found", 'toast_class': "danger-subtle"}, 404

View File

@ -0,0 +1,555 @@
import os
import ast
import uuid
import shutil
import datetime
import subprocess
from .. import db
from ..db_class.db import *
from ..utils.utils import create_specific_dir
from sqlalchemy import desc, and_
from flask import request, send_file
from werkzeug.utils import secure_filename
from ..notification import notification_core as NotifModel
from . import common_core as CommonModel
UPLOAD_FOLDER = os.path.join(os.getcwd(), "uploads")
FILE_FOLDER = os.path.join(UPLOAD_FOLDER, "files")
TEMP_FOLDER = os.path.join(os.getcwd(), "temp")
def delete_task(tid, current_user):
"""Delete a task by is id"""
task = CommonModel.get_task(tid)
if task is not None:
for file in task.files:
try:
os.remove(os.path.join(FILE_FOLDER, file.uuid))
except:
return False
db.session.delete(file)
db.session.commit()
case = CommonModel.get_case(task.case_id)
task_users = Task_User.query.where(Task_User.task_id==task.id).all()
for task_user in task_users:
user = User.query.get(task_user.user_id)
NotifModel.create_notification_user(f"Task '{task.id}-{task.title}' of case '{case.id}-{case.title}' was deleted", task.case_id, user_id=user.id, html_icon="fa-solid fa-trash")
Task_Tags.query.filter_by(task_id=task.id).delete()
Task_Galaxy_Tags.query.filter_by(task_id=task.id).delete()
Task_User.query.filter_by(task_id=task.id).delete()
Task_Connector_Instance.query.filter_by(task_id=task.id).delete()
db.session.delete(task)
CommonModel.update_last_modif(task.case_id)
db.session.commit()
CommonModel.save_history(case.uuid, current_user, f"Task '{task.title}' deleted")
return True
return False
def complete_task(tid, current_user):
"""Complete task by is id"""
task = CommonModel.get_task(tid)
if task is not None:
task.completed = not task.completed
case = CommonModel.get_case(task.case_id)
task_users = Task_User.query.where(Task_User.task_id==task.id).all()
if task.completed:
task.status_id = Status.query.filter_by(name="Finished").first().id
message = f"Task '{task.id}-{task.title}' of case '{case.id}-{case.title}' completed"
else:
task.status_id = Status.query.filter_by(name="Created").first().id
message = f"Task '{task.id}-{task.title}' of case '{case.id}-{case.title}' revived"
for task_user in task_users:
user = User.query.get(task_user.user_id)
if task.completed:
message = f"Task '{task.id}-{task.title}' of case '{case.id}-{case.title}' completed"
NotifModel.create_notification_user(message, task.case_id, user_id=user.id, html_icon="fa-solid fa-check")
else:
message = f"Task '{task.id}-{task.title}' of case '{case.id}-{case.title}' revived"
NotifModel.create_notification_user(message, task.case_id, user_id=user.id, html_icon="fa-solid fa-heart-circle-bolt")
CommonModel.update_last_modif(task.case_id)
CommonModel.update_last_modif_task(task.id)
db.session.commit()
CommonModel.save_history(case.uuid, current_user, f"Task '{task.title}' completed")
return True
return False
def create_task(form_dict, cid, current_user):
"""Add a task to the DB"""
if "template_select" in form_dict and not 0 in form_dict["template_select"]:
template = Task_Template.query.get(form_dict["template_select"])
task = Task(
uuid=str(uuid.uuid4()),
title=template.title,
description=template.description,
url=template.url,
creation_date=datetime.datetime.now(tz=datetime.timezone.utc),
last_modif=datetime.datetime.now(tz=datetime.timezone.utc),
case_id=cid,
status_id=1
)
db.session.add(task)
db.session.commit()
for t_t in Task_Template_Tags.query.filter_by(task_id=task.id).all():
task_tag = Task_Tags(
task_id=task.id,
tag_id=t_t.tag_id
)
db.session.add(task_tag)
db.session.commit()
for t_t in Task_Template_Galaxy_Tags.query.filter_by(task_id=task.id).all():
task_tag = Task_Galaxy_Tags(
task_id=task.id,
cluster_id=t_t.cluster_id
)
db.session.add(task_tag)
db.session.commit()
for t_t in Task_Template_Connector_Instance.query.filter_by(template_id=task.id).all():
task_instance = Task_Connector_Instance(
task_id=task.id,
instance_id=t_t.instance_id
)
db.session.add(task_instance)
db.session.commit()
else:
deadline = CommonModel.deadline_check(form_dict["deadline_date"], form_dict["deadline_time"])
## Check if instance is from current user
for instance in form_dict["connectors"]:
instance = CommonModel.get_instance_by_name(instance)
if not CommonModel.get_user_instance_by_instance(instance.id):
return False
task = Task(
uuid=str(uuid.uuid4()),
title=form_dict["title"],
description=form_dict["description"],
url=form_dict["url"],
creation_date=datetime.datetime.now(tz=datetime.timezone.utc),
last_modif=datetime.datetime.now(tz=datetime.timezone.utc),
deadline=deadline,
case_id=cid,
status_id=1
)
db.session.add(task)
db.session.commit()
for tags in form_dict["tags"]:
tag = CommonModel.get_tag(tags)
task_tag = Task_Tags(
tag_id=tag.id,
task_id=task.id
)
db.session.add(task_tag)
db.session.commit()
for clusters in form_dict["clusters"]:
cluster = CommonModel.get_cluster_by_name(clusters)
task_galaxy_tag = Task_Galaxy_Tags(
cluster_id=cluster.id,
task_id=task.id
)
db.session.add(task_galaxy_tag)
db.session.commit()
for instance in form_dict["connectors"]:
instance = CommonModel.get_instance_by_name(instance)
task_instance = Task_Connector_Instance(
task_id=task.id,
instance_id=instance.id
)
db.session.add(task_instance)
db.session.commit()
CommonModel.update_last_modif(cid)
case = CommonModel.get_case(cid)
CommonModel.save_history(case.uuid, current_user, f"Task '{task.title}' Created")
return task
def edit_task_core(form_dict, tid, current_user):
"""Edit a task to the DB"""
task = CommonModel.get_task(tid)
deadline = CommonModel.deadline_check(form_dict["deadline_date"], form_dict["deadline_time"])
## Check if instance is from current user
for instance in form_dict["connectors"]:
instance = CommonModel.get_instance_by_name(instance)
if not CommonModel.get_user_instance_by_instance(instance.id):
return False
task.title = form_dict["title"]
task.description=form_dict["description"]
task.url=form_dict["url"]
task.deadline=deadline
## Tags
task_tag_db = Task_Tags.query.filter_by(task_id=task.id).all()
for tags in form_dict["tags"]:
tag = CommonModel.get_tag(tags)
if not tags in task_tag_db:
task_tag = Task_Tags(
tag_id=tag.id,
task_id=task.id
)
db.session.add(task_tag)
db.session.commit()
for c_t_db in task_tag_db:
if not c_t_db in form_dict["tags"]:
Task_Tags.query.filter_by(id=c_t_db.id).delete()
db.session.commit()
## Clusters
task_tag_db = Task_Galaxy_Tags.query.filter_by(task_id=task.id).all()
for clusters in form_dict["clusters"]:
cluster = CommonModel.get_cluster_by_name(clusters)
if not clusters in task_tag_db:
task_tag = Task_Galaxy_Tags(
cluster_id=cluster.id,
task_id=task.id
)
db.session.add(task_tag)
db.session.commit()
for c_t_db in task_tag_db:
if not c_t_db in form_dict["clusters"]:
Task_Galaxy_Tags.query.filter_by(id=c_t_db.id).delete()
db.session.commit()
## Connectors
task_connector_db = Task_Connector_Instance.query.filter_by(task_id=task.id).all()
for connectors in form_dict["connectors"]:
instance = CommonModel.get_instance_by_name(connectors)
if not connectors in task_connector_db:
task_tag = Task_Connector_Instance(
instance_id=instance.id,
task_id=task.id
)
db.session.add(task_tag)
db.session.commit()
for c_t_db in task_connector_db:
if not c_t_db in form_dict["connectors"]:
Task_Connector_Instance.query.filter_by(id=c_t_db.id).delete()
db.session.commit()
CommonModel.update_last_modif(task.case_id)
CommonModel.update_last_modif_task(task.id)
db.session.commit()
case = CommonModel.get_case(task.case_id)
CommonModel.save_history(case.uuid, current_user, f"Task '{task.title}' edited")
def add_file_core(task, files_list, current_user):
create_specific_dir(UPLOAD_FOLDER)
create_specific_dir(FILE_FOLDER)
for file in files_list:
if files_list[file].filename:
uuid_loc = str(uuid.uuid4())
filename = secure_filename(files_list[file].filename)
try:
file_data = request.files[file].read()
with open(os.path.join(FILE_FOLDER, uuid_loc), "wb") as write_file:
write_file.write(file_data)
except Exception as e:
print(e)
return False
f = File(
name=filename,
task_id=task.id,
uuid = uuid_loc
)
db.session.add(f)
CommonModel.update_last_modif(task.case_id)
CommonModel.update_last_modif_task(task.id)
db.session.commit()
case = CommonModel.get_case(task.case_id)
CommonModel.save_history(case.uuid, current_user, f"File added for task '{task.title}'")
return True
def modif_note_core(tid, current_user, notes):
"""Modify a noe of a task to the DB"""
task = CommonModel.get_task(tid)
if task:
task.notes = notes
CommonModel.update_last_modif(task.case_id)
CommonModel.update_last_modif_task(task.id)
db.session.commit()
case = CommonModel.get_case(task.case_id)
CommonModel.save_history(case.uuid, current_user, f"Notes for '{task.title}' modified")
return True
return False
def assign_task(tid, user, current_user, flag_current_user):
"""Assign current user to a task"""
task = CommonModel.get_task(tid)
case = CommonModel.get_case(task.case_id)
if task:
if type(user) == str or type(user) == int:
user = User.query.get(user)
task_user = Task_User(task_id=task.id, user_id=user.id)
if not flag_current_user:
NotifModel.create_notification_user(f"You have been assign to: '{task.id}-{task.title}' of case '{case.id}-{case.title}'", task.case_id, user_id=user.id, html_icon="fa-solid fa-hand")
if not Task_User.query.filter_by(task_id=task.id, user_id=user.id).first():
db.session.add(task_user)
CommonModel.update_last_modif(task.case_id)
CommonModel.update_last_modif_task(task.id)
db.session.commit()
CommonModel.save_history(case.uuid, current_user, f"Task '{task.id}-{task.title}' assigned to {user.first_name} {user.last_name}")
return True
return False
return False
def get_users_assign_task(task_id, user):
"""Return users assigned to a task"""
task_users = Task_User.query.filter_by(task_id=task_id).all()
users = list()
flag = False
for task_user in task_users:
u = User.query.get(task_user.user_id)
# if current user is assigned to the task
if u.id == user.id:
flag = True
users.append(u.to_json())
return users, flag
def remove_assign_task(tid, user, current_user, flag_current_user):
"""Remove current user to the assignement to a task"""
task = CommonModel.get_task(tid)
case = CommonModel.get_case(task.case_id)
if task:
if type(user) == int or type(user) == str:
user = User.query.get(user)
task_users = Task_User.query.filter_by(task_id=task.id, user_id=user.id).all()
if not flag_current_user:
NotifModel.create_notification_user(f"Your assignment have been removed: '{task.id}-{task.title}' of case '{case.id}-{case.title}'", task.case_id, user_id=user.id, html_icon="fa-solid fa-handshake-slash")
for task_user in task_users:
db.session.delete(task_user)
CommonModel.update_last_modif(task.case_id)
CommonModel.update_last_modif_task(task.id)
db.session.commit()
CommonModel.save_history(case.uuid, current_user, f"Assignment '{task.title}' removed to {user.first_name} {user.last_name}")
return True
return False
def change_task_status(status, task, current_user):
task.status_id = status
CommonModel.update_last_modif(task.case_id)
CommonModel.update_last_modif_task(task.id)
db.session.commit()
case = CommonModel.get_case(task.case_id)
CommonModel.save_history(case.uuid, current_user, f"Status changed for task '{task.title}'")
return True
def download_file(file):
return send_file(os.path.join(FILE_FOLDER, file.uuid), as_attachment=True, download_name=file.name)
def delete_file(file, task, current_user):
try:
os.remove(os.path.join(FILE_FOLDER, file.uuid))
except:
return False
db.session.delete(file)
db.session.commit()
case = CommonModel.get_case(task.case_id)
CommonModel.save_history(case.uuid, current_user, f"File deleted for task '{task.title}'")
return True
def get_task_info(tasks_list, user):
tasks = list()
for task in tasks_list:
case = CommonModel.get_case(task.case_id)
users, is_current_user_assigned = get_users_assign_task(task.id, user)
file_list = list()
for file in task.files:
file_list.append(file.to_json())
finalTask = task.to_json()
finalTask["users"] = users
finalTask["is_current_user_assigned"] = is_current_user_assigned
finalTask["files"] = file_list
finalTask["case_title"] = case.title
finalTask["instances"] = list()
for task_connector in CommonModel.get_task_connectors(task.id):
loc_instance = CommonModel.get_instance(task_connector.instance_id).to_json()
loc_instance["icon"] = Icon_File.query.join(Connector_Icon, Connector_Icon.file_icon_id==Icon_File.id)\
.join(Connector, Connector.icon_id==Connector_Icon.id)\
.join(Connector_Instance, Connector_Instance.connector_id==Connector.id)\
.where(Connector_Instance.id==task_connector.instance_id)\
.first().uuid
finalTask["instances"].append(loc_instance)
tasks.append(finalTask)
return tasks
def build_task_query(completed, tags=None, taxonomies=None, galaxies=None, clusters=None, filter=None):
query = Task.query
conditions = [Task.completed == completed]
if tags or taxonomies:
query = query.join(Task_Tags, Task_Tags.task_id == Task.id)
query = query.join(Tags, Task_Tags.tag_id == Tags.id)
if tags:
tags = ast.literal_eval(tags)
conditions.append(Tags.name.in_(list(tags)))
if taxonomies:
taxonomies = ast.literal_eval(taxonomies)
query = query.join(Taxonomy, Taxonomy.id == Tags.taxonomy_id)
conditions.append(Taxonomy.name.in_(list(taxonomies)))
if clusters or galaxies:
query = query.join(Task_Galaxy_Tags, Task_Galaxy_Tags.task_id == Task.id)
query = query.join(Cluster, Task_Galaxy_Tags.cluster_id == Cluster.id)
if clusters:
clusters = ast.literal_eval(clusters)
conditions.append(Cluster.name.in_(list(clusters)))
if galaxies:
galaxies = ast.literal_eval(galaxies)
query = query.join(Galaxy, Galaxy.id == Cluster.galaxy_id)
conditions.append(Galaxy.name.in_(list(galaxies)))
if filter:
query.order_by(desc(filter))
return query.filter(and_(*conditions)).all()
def sort_by_status_task_core(case, user, taxonomies=[], galaxies=[], tags=[], clusters=[], or_and_taxo="true", or_and_galaxies="true", completed=False, no_info=False, filter=False):
tasks = build_task_query(completed, tags, taxonomies, galaxies, clusters, filter)
if tags:
tags = ast.literal_eval(tags)
if taxonomies:
taxonomies = ast.literal_eval(taxonomies)
if galaxies:
galaxies = ast.literal_eval(galaxies)
if clusters:
clusters = ast.literal_eval(clusters)
if tags or taxonomies or galaxies or clusters:
if or_and_taxo == "false":
glob_list = []
for task in tasks:
tags_db = task.to_json()["tags"]
loc_tag = [tag["name"] for tag in tags_db]
taxo_list = [Taxonomy.query.get(tag["taxonomy_id"]).name for tag in tags_db]
if (not tags or all(item in loc_tag for item in tags)) and \
(not taxonomies or all(item in taxo_list for item in taxonomies)):
glob_list.append(task)
tasks = glob_list
if or_and_galaxies == "false":
glob_list = []
for task in tasks:
clusters_db = task.to_json()["clusters"]
loc_cluster = [cluster["name"] for cluster in clusters_db]
galaxies_list = [Galaxy.query.get(cluster["galaxy_id"]).name for cluster in clusters_db]
if (not clusters or all(item in loc_cluster for item in clusters)) and \
(not galaxies or all(item in galaxies_list for item in galaxies)):
glob_list.append(task)
tasks = glob_list
else:
tasks = Task.query.filter_by(case_id=case.id, completed=completed).all()
if no_info:
return tasks
return get_task_info(tasks, user)
def sort_tasks_by_filter(case, user, filter, taxonomies=[], galaxies=[], tags=[], clusters=[], or_and_taxo="true", or_and_galaxies="true", completed=False):
tasks_list = sort_by_status_task_core(case, user, taxonomies, galaxies, tags, clusters, or_and_taxo, or_and_galaxies, completed, no_info=True, filter=filter)
loc_list = list()
if filter == "assigned_tasks":
for task in tasks_list:
if Task_User.query.filter_by(task_id=task.id).first():
loc_list.append(task)
tasks_list = loc_list
elif filter == "my_assignment":
for task in tasks_list:
if Task_User.query.filter_by(task_id=task.id, user_id=user.id).first():
task.is_current_user_assigned = True
loc_list.append(task)
tasks_list = loc_list
elif filter == "deadline":
# for deadline filter, only task with a deadline defined is required
loc = list()
for task in tasks_list:
if getattr(task, filter):
loc.append(task)
tasks_list = loc
else:
# status, last_modif, title
tasks_list.sort(key=lambda x: getattr(x, filter))
return get_task_info(tasks_list, user)
def export_notes(task, type_req):
if not os.path.isdir(TEMP_FOLDER):
os.mkdir(TEMP_FOLDER)
download_filename = f"export_note_task_{task.id}.{type_req}"
temp_md = os.path.join(TEMP_FOLDER, "index.md")
temp_export = os.path.join(TEMP_FOLDER, f"output.{type_req}")
with open(temp_md, "w")as write_file:
write_file.write(task.notes)
if type_req == "pdf":
process = subprocess.Popen(["pandoc", temp_md, "--pdf-engine=xelatex", "-V", "colorlinks=true", "-V", "linkcolor=blue", "-V", "urlcolor=red", "-V", "tocolor=gray"\
"--number-sections", "--toc", "--template", "eisvogel", "-o", temp_export, "--filter=pandoc-mermaid"], stdout=subprocess.PIPE)
elif type_req == "docx":
process = subprocess.Popen(["pandoc", temp_md, "-o", temp_export, "--filter=mermaid-filter"], stdout=subprocess.PIPE)
process.wait()
try:
shutil.rmtree(os.path.join(os.getcwd(), "mermaid-images"))
except:
pass
return send_file(temp_export, as_attachment=True, download_name=download_filename)

View File

@ -0,0 +1,49 @@
from .. import db
class Module(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, index=True, unique=True)
description = db.Column(db.String)
is_active = db.Column(db.Boolean, default=True)
request_on_query = db.Column(db.Boolean, default=False)
def to_json(self):
json_dict = {
"id": self.id,
"name": self.name,
"description": self.description,
"is_active": self.is_active,
"request_on_query": self.request_on_query
}
return json_dict
class Session_db(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
uuid = db.Column(db.String(36), index=True, unique=True)
glob_query = db.Column(db.String)
query_enter = db.Column(db.String)
input_query = db.Column(db.String)
config_module=db.Column(db.String)
result=db.Column(db.String)
nb_errors = db.Column(db.Integer, index=True)
def to_json(self):
return
class History(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
session_id = db.Column(db.Integer, index=True)
class Config(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, index=True, unique=True)
class Module_Config(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
module_id = db.Column(db.Integer, index=True)
config_id = db.Column(db.Integer, index=True)
value = db.Column(db.String, index=True)

138
webiste/app/home.py Normal file
View File

@ -0,0 +1,138 @@
from flask import Flask, Blueprint, render_template, request, jsonify
from . import home_core as HomeModel
from . import session as SessionModel
home_blueprint = Blueprint(
'home',
__name__,
template_folder='templates',
static_folder='static'
)
@home_blueprint.route("/")
def home():
return render_template("home.html")
@home_blueprint.route("/get_modules")
def get_modules():
"""Return all modules available"""
expansion = ""
hover = ""
if "expansion" in request.args:
expansion = request.args.get("expansion")
if "hover" in request.args:
hover = request.args.get("hover")
res = HomeModel.get_modules(expansion, hover)
if "message" in res:
return res, 404
return res, 200
@home_blueprint.route("/get_list_misp_attributes")
def get_list_misp_attributes():
"""Return all misp attributes for input and output"""
expansion = ""
hover = ""
if "expansion" in request.args:
expansion = request.args.get("expansion")
if "hover" in request.args:
hover = request.args.get("hover")
res = HomeModel.get_list_misp_attributes(expansion, hover)
if "message" in res:
return res, 404
return res, 200
@home_blueprint.route("/run_modules", methods=['POST'])
def run_modules():
"""Run modules"""
if "query" in request.json:
if "input" in request.json:
if "expansion" in request.json or "hover" in request.json:
session = SessionModel.Session_class(request.json)
session.start()
SessionModel.sessions.append(session)
return jsonify(session.status()), 201
return {"message": "Need a module type"}, 400
return {"message": "Need an input (misp attribute)"}, 400
return {"message": "Need to type something"}, 400
@home_blueprint.route("/status/<sid>")
def status(sid):
"""Status of <sid> queue"""
sess = HomeModel.get_session(sid)
if sess:
return jsonify(HomeModel.get_status_db(sess))
else:
for s in SessionModel.sessions:
if s.id == sid:
return jsonify(s.status())
return jsonify({'message': 'Scan session not found'}), 404
@home_blueprint.route("/result/<sid>")
def result(sid):
"""Result of <sid> queue"""
sess = HomeModel.get_session(sid)
if sess:
return jsonify(HomeModel.get_result_db(sess))
else:
for s in SessionModel.sessions:
if s.id == sid:
return jsonify(s.get_result())
return jsonify({'message': 'Scan session not found'}), 404
@home_blueprint.route("/modules_config")
def modules_config():
"""List all modules for configuration"""
return render_template("modules_config.html")
@home_blueprint.route("/modules_config_data")
def modules_config_data():
"""List all modules for configuration"""
modules_config = HomeModel.get_modules_config()
return modules_config, 200
@home_blueprint.route("/change_config", methods=["POST"])
def change_config():
"""Change configuation for a module"""
if "module_name" in request.json["result_dict"]:
res = HomeModel.change_config_core(request.json["result_dict"])
if res:
return {'message': 'Config changed', 'toast_class': "success-subtle"}, 200
return {'message': 'Something went wrong', 'toast_class': "danger-subtle"}, 400
return {'message': 'Need to pass "module_name"', 'toast_class': "warning-subtle"}, 400
@home_blueprint.route("/change_status", methods=["GET"])
def change_status():
"""Change the status of a module, active or unactive"""
if "module_id" in request.args:
res = HomeModel.change_status_core(request.args.get("module_id"))
if res:
return {'message': 'Module status changed', 'toast_class': "success-subtle"}, 200
return {'message': 'Something went wrong', 'toast_class': "danger-subtle"}, 400
return {'message': 'Need to pass "module_id"', 'toast_class': "warning-subtle"}, 400
@home_blueprint.route("/history", methods=["GET"])
def history():
"""View all history"""
return render_template("history.html")
@home_blueprint.route("/get_history", methods=["GET"])
def get_history():
"""Get all history"""
histories = HomeModel.get_history()
return histories

133
webiste/app/home_core.py Normal file
View File

@ -0,0 +1,133 @@
import json
from .utils.utils import query_get_module
from . import db
from .db_class.db import History, Module, Config, Module_Config, Session_db
def get_module(mid):
return Module.query.get(mid)
def get_module_by_name(name):
return Module.query.filter_by(name=name).first()
def get_config(cid):
return Config.query.get(cid)
def get_config_by_name(name):
return Config.query.filter_by(name=name).first()
def get_module_config_module(mid):
return Module_Config.query.filter_by(module_id=mid).all()
def get_module_config_both(mid, cid):
return Module_Config.query.filter_by(module_id=mid, config_id=cid).first()
def get_session(sid):
return Session_db.query.filter_by(uuid=sid).first()
def get_modules(expansion, hover):
res = query_get_module()
if not "message" in res:
loc_list = dict()
loc_list["expansion"] = list()
loc_list["hover"] = list()
for module in res:
module_db = get_module_by_name(module["name"])
module_loc = module
module_loc["request_on_query"] = module_db.request_on_query
if module_db.is_active:
if expansion:
if "expansion" in module["meta"]["module-type"]:
loc_list["expansion"].append(module_loc)
if hover:
if "hover" in module["meta"]["module-type"]:
loc_list["hover"].append(module_loc)
loc_list["expansion"].sort(key=lambda x: x["name"])
loc_list["hover"].sort(key=lambda x: x["name"])
return loc_list
return res
def util_get_attr(module, loc_list):
if "input" in module["mispattributes"]:
for input in module["mispattributes"]["input"]:
if not input in loc_list:
loc_list.append(input)
return loc_list
def get_list_misp_attributes(expansion, hover):
res = query_get_module()
if not "message" in res:
loc_list = list()
for module in res:
if get_module_by_name(module["name"]).is_active:
if expansion:
if "expansion" in module["meta"]["module-type"]:
loc_list = util_get_attr(module, loc_list)
if hover:
if "hover" in module["meta"]["module-type"]:
loc_list = util_get_attr(module, loc_list)
loc_list.sort()
return loc_list
return res
def get_modules_config():
modules = Module.query.order_by(Module.name).all()
modules_list = []
for module in modules:
loc_module = module.to_json()
loc_module["config"] = []
mcs = Module_Config.query.filter_by(module_id=module.id).all()
for mc in mcs:
conf = Config.query.get(mc.config_id)
loc_module["config"].append({conf.name: mc.value})
modules_list.append(loc_module)
return modules_list
def change_config_core(request_json):
module = get_module_by_name(request_json["module_name"])
for element in request_json:
if not element == "module_name":
config = get_config_by_name(element)
if config:
m_c = get_module_config_both(module.id, config.id)
m_c.value = request_json[element]
db.session.commit()
module.request_on_query = request_json["request_on_query"]
db.session.commit()
return True
def change_status_core(module_id):
module = get_module(module_id)
module.is_active = not module.is_active
db.session.commit()
return True
def get_status_db(session):
glob_query = json.loads(session.glob_query)
result = json.loads(session.result)
return{
'id': session.uuid,
'total': len(glob_query),
'complete': len(glob_query),
'remaining': 0,
'registered': len(result),
'stopped' : True,
"nb_errors": session.nb_errors
}
def get_result_db(session):
return json.loads(session.result)
def get_history():
histories_list = list()
histories = History.query.all()
for history in histories:
session = Session_db.query.get(history.session_id)
histories_list.append({"uuid": session.uuid, "query": session.query_enter, "modules": json.loads(session.glob_query), "input": session.input_query})
return histories_list

161
webiste/app/session.py Normal file
View File

@ -0,0 +1,161 @@
import json
from queue import Queue
from threading import Thread
from uuid import uuid4
from .utils.utils import query_post_query, query_get_module
from . import home_core as HomeModel
import uuid
from . import db
from .db_class.db import History, Session_db
from sqlalchemy import func
sessions = list()
class Session_class:
def __init__(self, request_json) -> None:
self.id = str(uuid4())
self.thread_count = 4
self.jobs = Queue(maxsize=0)
self.threads = []
self.stopped = False
self.result_stopped = dict()
self.result = dict()
self.expansion = self.expansion_setter(request_json)
self.hover = self.hover_setter(request_json)
self.query = request_json["query"]
self.input_query = request_json["input"]
self.glob_query = self.expansion + self.hover
self.nb_errors = 0
self.config_module = self.config_module_setter(request_json)
def expansion_setter(self, request_json):
if "expansion" in request_json:
return request_json["expansion"]
return []
def hover_setter(self, request_json):
if "hover" in request_json:
return request_json["hover"]
return []
def config_module_setter(self, request_json):
if request_json["config"]:
for query in self.glob_query:
if not query in request_json["config"]:
request_json["config"][query] = {}
module = HomeModel.get_module_by_name(query)
mcs = HomeModel.get_module_config_module(module.id)
for mc in mcs:
config_db = HomeModel.get_config(mc.config_id)
request_json["config"][query][config_db.name] = mc.value
return request_json["config"]
return {}
def start(self):
"""Start all worker"""
for i in range(len(self.glob_query)):
#need the index and the url in each queue item.
self.jobs.put((i, self.glob_query[i]))
for _ in range(self.thread_count):
worker = Thread(target=self.process)
worker.daemon = True
worker.start()
self.threads.append(worker)
def status(self):
"""Status of the current queue"""
if self.jobs.empty():
self.stop()
total = len(self.glob_query)
remaining = max(self.jobs.qsize(), len(self.threads))
complete = total - remaining
registered = len(self.result)
return {
'id': self.id,
'total': total,
'complete': complete,
'remaining': remaining,
'registered': registered,
'stopped' : self.stopped,
"nb_errors": self.nb_errors
}
def stop(self):
"""Stop the current queue and worker"""
self.jobs.queue.clear()
for worker in self.threads:
worker.join(3.5)
self.threads.clear()
sessions.remove(self)
self.save_info()
def process(self):
"""Threaded function for queue processing."""
while not self.jobs.empty():
work = self.jobs.get()
modules = query_get_module()
loc_query = {}
# If Misp format
for module in modules:
if module["name"] == work[1]:
if "format" in module["mispattributes"]:
loc_query = {
"type": self.input_query,
"value": self.query,
"uuid": str(uuid.uuid4())
}
break
loc_config = {}
if work[1] in self.config_module:
loc_config = self.config_module[work[1]]
if loc_query:
send_to = {"module": work[1], "attribute": loc_query, "config": loc_config}
else:
send_to = {"module": work[1], self.input_query: self.query, "config": loc_config}
res = query_post_query(send_to)
print(res)
if "error" in res:
self.nb_errors += 1
self.result[work[1]] = res
self.jobs.task_done()
return True
def get_result(self):
return self.result
def save_info(self):
s = Session_db(
uuid=str(self.id),
glob_query=json.dumps(self.glob_query),
query_enter=self.query,
input_query=self.input_query,
config_module=json.dumps(self.config_module),
result=json.dumps(self.result),
nb_errors=self.nb_errors
)
db.session.add(s)
db.session.commit()
h = History(
session_id=s.id
)
db.session.add(h)
db.session.commit()
histories = History.query.all()
while len(histories) > 100:
history = History.query.filter_by(func.min(History.id))
history.delete()
histories = History.query.all()
return

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,591 @@
/*!
* Bootstrap Reboot v5.3.0-alpha1 (https://getbootstrap.com/)
* Copyright 2011-2022 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text: #0a58ca;
--bs-secondary-text: #6c757d;
--bs-success-text: #146c43;
--bs-info-text: #087990;
--bs-warning-text: #997404;
--bs-danger-text: #b02a37;
--bs-light-text: #6c757d;
--bs-dark-text: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #f8f9fa;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #e9ecef;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg-rgb: 255, 255, 255;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-2xl: 2rem;
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(var(--bs-body-color-rgb), 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(var(--bs-body-color-rgb), 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(var(--bs-body-color-rgb), 0.075);
--bs-emphasis-color: #000;
--bs-form-control-bg: var(--bs-body-bg);
--bs-form-control-disabled-bg: var(--bs-secondary-bg);
--bs-highlight-bg: #fff3cd;
--bs-breakpoint-xs: 0;
--bs-breakpoint-sm: 576px;
--bs-breakpoint-md: 768px;
--bs-breakpoint-lg: 992px;
--bs-breakpoint-xl: 1200px;
--bs-breakpoint-xxl: 1400px;
}
[data-bs-theme=dark] {
--bs-body-color: #adb5bd;
--bs-body-color-rgb: 173, 181, 189;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #f8f9fa;
--bs-emphasis-color-rgb: 248, 249, 250;
--bs-secondary-color: rgba(173, 181, 189, 0.75);
--bs-secondary-color-rgb: 173, 181, 189;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(173, 181, 189, 0.5);
--bs-tertiary-color-rgb: 173, 181, 189;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-emphasis-color: #fff;
--bs-primary-text: #6ea8fe;
--bs-secondary-text: #dee2e6;
--bs-success-text: #75b798;
--bs-info-text: #6edff6;
--bs-warning-text: #ffda6a;
--bs-danger-text: #ea868f;
--bs-light-text: #f8f9fa;
--bs-dark-text: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #212529;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #495057;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #055160;
--bs-warning-border-subtle: #664d03;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: #fff;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #9ec5fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 158, 197, 254;
--bs-code-color: #e685b5;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color, inherit);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,588 @@
/*!
* Bootstrap Reboot v5.3.0-alpha1 (https://getbootstrap.com/)
* Copyright 2011-2022 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text: #0a58ca;
--bs-secondary-text: #6c757d;
--bs-success-text: #146c43;
--bs-info-text: #087990;
--bs-warning-text: #997404;
--bs-danger-text: #b02a37;
--bs-light-text: #6c757d;
--bs-dark-text: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #f8f9fa;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #e9ecef;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg-rgb: 255, 255, 255;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-2xl: 2rem;
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(var(--bs-body-color-rgb), 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(var(--bs-body-color-rgb), 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(var(--bs-body-color-rgb), 0.075);
--bs-emphasis-color: #000;
--bs-form-control-bg: var(--bs-body-bg);
--bs-form-control-disabled-bg: var(--bs-secondary-bg);
--bs-highlight-bg: #fff3cd;
--bs-breakpoint-xs: 0;
--bs-breakpoint-sm: 576px;
--bs-breakpoint-md: 768px;
--bs-breakpoint-lg: 992px;
--bs-breakpoint-xl: 1200px;
--bs-breakpoint-xxl: 1400px;
}
[data-bs-theme=dark] {
--bs-body-color: #adb5bd;
--bs-body-color-rgb: 173, 181, 189;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #f8f9fa;
--bs-emphasis-color-rgb: 248, 249, 250;
--bs-secondary-color: rgba(173, 181, 189, 0.75);
--bs-secondary-color-rgb: 173, 181, 189;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(173, 181, 189, 0.5);
--bs-tertiary-color-rgb: 173, 181, 189;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-emphasis-color: #fff;
--bs-primary-text: #6ea8fe;
--bs-secondary-text: #dee2e6;
--bs-success-text: #75b798;
--bs-info-text: #6edff6;
--bs-warning-text: #ffda6a;
--bs-danger-text: #ea868f;
--bs-light-text: #f8f9fa;
--bs-dark-text: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #212529;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #495057;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #055160;
--bs-warning-border-subtle: #664d03;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: #fff;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #9ec5fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 158, 197, 254;
--bs-code-color: #e685b5;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color, inherit);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,172 @@
body {
background-color: #fbfbfb;
}
@media (min-width: 991.98px) {
main {
padding-left: 200px;
}
}
span#goTop, span#project-version{
position: fixed;
right: 1em;
bottom: 1em;
background-color: #fafbfc;
}
.select2-container {
padding: 5px
}
.select2-selection__choice{
background-color: #ced4da;
filter: drop-shadow(-1px 1px 1px rgba(50, 50, 0, 0.5));
}
div#searchbox {
display: flex;
margin: 0 auto 0 auto;
}
#process-query {
font-size: 1.1em;
height: 2.8em;
border: 1px solid #ccc;
border-radius: 5px 0 0 5px;
padding-left: .75em;
width: 80%;
outline: none;
}
button#query {
color: white;
background-color: #0d6efd;
border: 0;
border-radius: 0 5px 5px 0;
height: 2.8em;
font-size: 1.1em;
font-weight: 600;
flex-grow: 1;
outline: none;
}
button#query:hover {
background-color: #2779bd;
}
progress {
-webkit-appearance: none;
-moz-appearance: none;
height: 5px;
width: 100%;
background-color: transparent;
border: 0;
margin: 5px 0;
}
progress::-webkit-progress-bar {
background-color: transparent;
}
progress::-webkit-progress-value {
background-color: #3490dc;
}
progress::-moz-progress-bar {
background-color: #3490dc;
}
span#status {
float: right;
font-size: 80%;
}
.checkbox-type-module{
margin-left: 5px
}
/*JSON Parser*/
#core-format-picker {
padding: 5px;
border-radius: 5px;
background-color: #f9f9f9;
border: 1px solid #e1e1e1;
}
#core-format-picker .selectable-key {
font-weight: bold;
padding: 2px 3px;
}
#core-format-picker .selectable-key:hover {
box-shadow: 0 0 5px #00000077;
cursor: pointer;
}
#core-format-picker .selectable-value {
padding: 2px 3px;
}
#core-format-picker .selectable-value:hover {
box-shadow: 0 0 5px #00000077;
font-weight: bold;
cursor: pointer;
}
#core-format-picker .children-counter {
background-color: #6b6b6b;
color: #ffffff;
padding: 2px 3px;
margin: 0px 0.25rem;
font-size: smaller;
border-radius: 3px;
}
#core-format-picker .collaspe-button {
cursor: pointer;
}
#core-format-picker .collaspe-button:hover {
color: #292929;
cursor: pointer;
}
/**/
.round-button {
width: 3%;
}
.round-button-circle {
padding-bottom: 100%;
border-radius: 50%;
background: #e13333;
box-shadow: 0 0 3px gray;
}
.round-button-circle:hover {
background:#c41313;
cursor: pointer;
}
.round-button a {
float:left;
width:100%;
padding-top:50%;
padding-bottom:50%;
line-height:1em;
margin-top:-0.5em;
text-align:center;
color:#e2eaf3;
text-decoration:none;
}
.side-panel-config {
background-color: white;
box-shadow: rgba(0, 0, 0, 0.05) 0px 2px 5px 0px, rgba(0, 0, 0, 0.05) 0px 2px 10px 0px;
border-radius: 10px;
height: 75vh;
position: fixed;
max-width: calc((100%) / 2 - 1*var(--bs-gutter-x) * .5);
right: calc(var(--bs-gutter-x) * .5);
}
@media (min-width: 992px) {
.side-panel-config {
max-width: calc((100% - 200px) / 2 - 1*var(--bs-gutter-x) * .5);
}
}

1314
webiste/app/static/css/jquery-ui.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
.json-view-item:not(.root-item){margin-left:15px}.value-key{color:var(--jtv-valueKey-color);font-weight:600;margin-left:10px;border-radius:2px;white-space:nowrap;padding:5px 5px 5px 10px}.value-key.can-select{cursor:pointer}.value-key.can-select:hover{background-color:#00000014}.value-key.can-select:focus{outline:2px solid var(--jtv-hover-color)}.data-key{font-size:100%;font-family:inherit;border:0;background-color:transparent;width:100%;color:var(--jtv-key-color);display:flex;align-items:center;border-radius:2px;font-weight:600;cursor:pointer;white-space:nowrap;padding:5px}.data-key:hover{background-color:var(--jtv-hover-color)}.data-key:focus{outline:2px solid var(--jtv-hover-color)}.data-key::-moz-focus-inner{border:0}.data-key .properties{font-weight:300;opacity:.9;margin-left:4px;-webkit-user-select:none;user-select:none}.chevron-arrow{flex-shrink:0;border-right:2px solid var(--jtv-arrow-color);border-bottom:2px solid var(--jtv-arrow-color);width:var(--jtv-arrow-size);height:var(--jtv-arrow-size);margin-right:20px;margin-left:5px;transform:rotate(-45deg)}.chevron-arrow.opened{margin-top:-3px;transform:rotate(45deg)}.root-item[data-v-1cbd6770]{--jtv-key-color: #0977e6;--jtv-valueKey-color: #073642;--jtv-string-color: #268bd2;--jtv-number-color: #2aa198;--jtv-boolean-color: #cb4b16;--jtv-null-color: #6c71c4;--jtv-arrow-size: 6px;--jtv-arrow-color: #444;--jtv-hover-color: rgba(0, 0, 0, .1);margin-left:0;width:100%;height:auto}.root-item.dark[data-v-1cbd6770]{--jtv-key-color: #80d8ff;--jtv-valueKey-color: #fdf6e3;--jtv-hover-color: rgba(255, 255, 255, .1);--jtv-arrow-color: #fdf6e3}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
/* Sidebar */
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
/* padding: 65px 0 0; */
box-shadow: 0 2px 5px 0 rgb(0 0 0 / 5%), 0 2px 10px 0 rgb(0 0 0 / 5%);
width: 200px;
z-index: 600;
}
.sidebar .active {
border-radius: 5px;
box-shadow: 0 2px 5px 0 rgb(0 0 0 / 16%), 0 2px 10px 0 rgb(0 0 0 / 12%);
}
#search-form{
margin-left: auto;
}
.container{
margin-left: 0;
}
.fa-circle {
font-size: 40%;
margin-right: 3px;
vertical-align: middle;
}

View File

@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2023 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2023 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,19 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
.far,
.fa-regular {
font-weight: 400; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}

View File

@ -0,0 +1,19 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 900;
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
.fas,
.fa-solid {
font-weight: 900; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}

View File

@ -0,0 +1,638 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:root, :host {
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Solid';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Regular';
--fa-font-light: normal 300 1em/1 'Font Awesome 6 Light';
--fa-font-thin: normal 100 1em/1 'Font Awesome 6 Thin';
--fa-font-duotone: normal 900 1em/1 'Font Awesome 6 Duotone';
--fa-font-sharp-solid: normal 900 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-regular: normal 400 1em/1 'Font Awesome 6 Sharp';
--fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands'; }
svg:not(:root).svg-inline--fa, svg:not(:host).svg-inline--fa {
overflow: visible;
box-sizing: content-box; }
.svg-inline--fa {
display: var(--fa-display, inline-block);
height: 1em;
overflow: visible;
vertical-align: -.125em; }
.svg-inline--fa.fa-2xs {
vertical-align: 0.1em; }
.svg-inline--fa.fa-xs {
vertical-align: 0em; }
.svg-inline--fa.fa-sm {
vertical-align: -0.07143em; }
.svg-inline--fa.fa-lg {
vertical-align: -0.2em; }
.svg-inline--fa.fa-xl {
vertical-align: -0.25em; }
.svg-inline--fa.fa-2xl {
vertical-align: -0.3125em; }
.svg-inline--fa.fa-pull-left {
margin-right: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-pull-right {
margin-left: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-li {
width: var(--fa-li-width, 2em);
top: 0.25em; }
.svg-inline--fa.fa-fw {
width: var(--fa-fw-width, 1.25em); }
.fa-layers svg.svg-inline--fa {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0; }
.fa-layers-text, .fa-layers-counter {
display: inline-block;
position: absolute;
text-align: center; }
.fa-layers {
display: inline-block;
height: 1em;
position: relative;
text-align: center;
vertical-align: -.125em;
width: 1em; }
.fa-layers svg.svg-inline--fa {
-webkit-transform-origin: center center;
transform-origin: center center; }
.fa-layers-text {
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
-webkit-transform-origin: center center;
transform-origin: center center; }
.fa-layers-counter {
background-color: var(--fa-counter-background-color, #ff253a);
border-radius: var(--fa-counter-border-radius, 1em);
box-sizing: border-box;
color: var(--fa-inverse, #fff);
line-height: var(--fa-counter-line-height, 1);
max-width: var(--fa-counter-max-width, 5em);
min-width: var(--fa-counter-min-width, 1.5em);
overflow: hidden;
padding: var(--fa-counter-padding, 0.25em 0.5em);
right: var(--fa-right, 0);
text-overflow: ellipsis;
top: var(--fa-top, 0);
-webkit-transform: scale(var(--fa-counter-scale, 0.25));
transform: scale(var(--fa-counter-scale, 0.25));
-webkit-transform-origin: top right;
transform-origin: top right; }
.fa-layers-bottom-right {
bottom: var(--fa-bottom, 0);
right: var(--fa-right, 0);
top: auto;
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: bottom right;
transform-origin: bottom right; }
.fa-layers-bottom-left {
bottom: var(--fa-bottom, 0);
left: var(--fa-left, 0);
right: auto;
top: auto;
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: bottom left;
transform-origin: bottom left; }
.fa-layers-top-right {
top: var(--fa-top, 0);
right: var(--fa-right, 0);
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: top right;
transform-origin: top right; }
.fa-layers-top-left {
left: var(--fa-left, 0);
right: auto;
top: var(--fa-top, 0);
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: top left;
transform-origin: top left; }
.fa-1x {
font-size: 1em; }
.fa-2x {
font-size: 2em; }
.fa-3x {
font-size: 3em; }
.fa-4x {
font-size: 4em; }
.fa-5x {
font-size: 5em; }
.fa-6x {
font-size: 6em; }
.fa-7x {
font-size: 7em; }
.fa-8x {
font-size: 8em; }
.fa-9x {
font-size: 9em; }
.fa-10x {
font-size: 10em; }
.fa-2xs {
font-size: 0.625em;
line-height: 0.1em;
vertical-align: 0.225em; }
.fa-xs {
font-size: 0.75em;
line-height: 0.08333em;
vertical-align: 0.125em; }
.fa-sm {
font-size: 0.875em;
line-height: 0.07143em;
vertical-align: 0.05357em; }
.fa-lg {
font-size: 1.25em;
line-height: 0.05em;
vertical-align: -0.075em; }
.fa-xl {
font-size: 1.5em;
line-height: 0.04167em;
vertical-align: -0.125em; }
.fa-2xl {
font-size: 2em;
line-height: 0.03125em;
vertical-align: -0.1875em; }
.fa-fw {
text-align: center;
width: 1.25em; }
.fa-ul {
list-style-type: none;
margin-left: var(--fa-li-margin, 2.5em);
padding-left: 0; }
.fa-ul > li {
position: relative; }
.fa-li {
left: calc(var(--fa-li-width, 2em) * -1);
position: absolute;
text-align: center;
width: var(--fa-li-width, 2em);
line-height: inherit; }
.fa-border {
border-color: var(--fa-border-color, #eee);
border-radius: var(--fa-border-radius, 0.1em);
border-style: var(--fa-border-style, solid);
border-width: var(--fa-border-width, 0.08em);
padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); }
.fa-pull-left {
float: left;
margin-right: var(--fa-pull-margin, 0.3em); }
.fa-pull-right {
float: right;
margin-left: var(--fa-pull-margin, 0.3em); }
.fa-beat {
-webkit-animation-name: fa-beat;
animation-name: fa-beat;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-bounce {
-webkit-animation-name: fa-bounce;
animation-name: fa-bounce;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); }
.fa-fade {
-webkit-animation-name: fa-fade;
animation-name: fa-fade;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-beat-fade {
-webkit-animation-name: fa-beat-fade;
animation-name: fa-beat-fade;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-flip {
-webkit-animation-name: fa-flip;
animation-name: fa-flip;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-shake {
-webkit-animation-name: fa-shake;
animation-name: fa-shake;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin {
-webkit-animation-name: fa-spin;
animation-name: fa-spin;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 2s);
animation-duration: var(--fa-animation-duration, 2s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin-reverse {
--fa-animation-direction: reverse; }
.fa-pulse,
.fa-spin-pulse {
-webkit-animation-name: fa-spin;
animation-name: fa-spin;
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, steps(8));
animation-timing-function: var(--fa-animation-timing, steps(8)); }
@media (prefers-reduced-motion: reduce) {
.fa-beat,
.fa-bounce,
.fa-fade,
.fa-beat-fade,
.fa-flip,
.fa-pulse,
.fa-shake,
.fa-spin,
.fa-spin-pulse {
-webkit-animation-delay: -1ms;
animation-delay: -1ms;
-webkit-animation-duration: 1ms;
animation-duration: 1ms;
-webkit-animation-iteration-count: 1;
animation-iteration-count: 1;
-webkit-transition-delay: 0s;
transition-delay: 0s;
-webkit-transition-duration: 0s;
transition-duration: 0s; } }
@-webkit-keyframes fa-beat {
0%, 90% {
-webkit-transform: scale(1);
transform: scale(1); }
45% {
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
transform: scale(var(--fa-beat-scale, 1.25)); } }
@keyframes fa-beat {
0%, 90% {
-webkit-transform: scale(1);
transform: scale(1); }
45% {
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
transform: scale(var(--fa-beat-scale, 1.25)); } }
@-webkit-keyframes fa-bounce {
0% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
10% {
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
100% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); } }
@keyframes fa-bounce {
0% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
10% {
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
100% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); } }
@-webkit-keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@-webkit-keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
-webkit-transform: scale(1);
transform: scale(1); }
50% {
opacity: 1;
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
-webkit-transform: scale(1);
transform: scale(1); }
50% {
opacity: 1;
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@-webkit-keyframes fa-flip {
50% {
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@keyframes fa-flip {
50% {
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@-webkit-keyframes fa-shake {
0% {
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg); }
4% {
-webkit-transform: rotate(15deg);
transform: rotate(15deg); }
8%, 24% {
-webkit-transform: rotate(-18deg);
transform: rotate(-18deg); }
12%, 28% {
-webkit-transform: rotate(18deg);
transform: rotate(18deg); }
16% {
-webkit-transform: rotate(-22deg);
transform: rotate(-22deg); }
20% {
-webkit-transform: rotate(22deg);
transform: rotate(22deg); }
32% {
-webkit-transform: rotate(-12deg);
transform: rotate(-12deg); }
36% {
-webkit-transform: rotate(12deg);
transform: rotate(12deg); }
40%, 100% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); } }
@keyframes fa-shake {
0% {
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg); }
4% {
-webkit-transform: rotate(15deg);
transform: rotate(15deg); }
8%, 24% {
-webkit-transform: rotate(-18deg);
transform: rotate(-18deg); }
12%, 28% {
-webkit-transform: rotate(18deg);
transform: rotate(18deg); }
16% {
-webkit-transform: rotate(-22deg);
transform: rotate(-22deg); }
20% {
-webkit-transform: rotate(22deg);
transform: rotate(22deg); }
32% {
-webkit-transform: rotate(-12deg);
transform: rotate(-12deg); }
36% {
-webkit-transform: rotate(12deg);
transform: rotate(12deg); }
40%, 100% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); } }
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
.fa-rotate-90 {
-webkit-transform: rotate(90deg);
transform: rotate(90deg); }
.fa-rotate-180 {
-webkit-transform: rotate(180deg);
transform: rotate(180deg); }
.fa-rotate-270 {
-webkit-transform: rotate(270deg);
transform: rotate(270deg); }
.fa-flip-horizontal {
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1); }
.fa-flip-vertical {
-webkit-transform: scale(1, -1);
transform: scale(1, -1); }
.fa-flip-both,
.fa-flip-horizontal.fa-flip-vertical {
-webkit-transform: scale(-1, -1);
transform: scale(-1, -1); }
.fa-rotate-by {
-webkit-transform: rotate(var(--fa-rotate-angle, none));
transform: rotate(var(--fa-rotate-angle, none)); }
.fa-stack {
display: inline-block;
vertical-align: middle;
height: 2em;
position: relative;
width: 2.5em; }
.fa-stack-1x,
.fa-stack-2x {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
z-index: var(--fa-stack-z-index, auto); }
.svg-inline--fa.fa-stack-1x {
height: 1em;
width: 1.25em; }
.svg-inline--fa.fa-stack-2x {
height: 2em;
width: 2.5em; }
.fa-inverse {
color: var(--fa-inverse, #fff); }
.sr-only,
.fa-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.sr-only-focusable:not(:focus),
.fa-sr-only-focusable:not(:focus) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.svg-inline--fa .fa-primary {
fill: var(--fa-primary-color, currentColor);
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa .fa-secondary {
fill: var(--fa-secondary-color, currentColor);
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-primary {
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-secondary {
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa mask .fa-primary,
.svg-inline--fa mask .fa-secondary {
fill: black; }
.fad.fa-inverse,
.fa-duotone.fa-inverse {
color: var(--fa-inverse, #fff); }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,26 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype");
unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype");
unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,22 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
@font-face {
font-family: 'Font Awesome 5 Brands';
font-display: block;
font-weight: 400;
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
@font-face {
font-family: 'Font Awesome 5 Free';
font-display: block;
font-weight: 900;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
@font-face {
font-family: 'Font Awesome 5 Free';
font-display: block;
font-weight: 400;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}

View File

@ -0,0 +1,152 @@
// animating icons
// --------------------------
.@{fa-css-prefix}-beat {
animation-name: ~'@{fa-css-prefix}-beat';
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, ease-in-out)';
}
.@{fa-css-prefix}-bounce {
animation-name: ~'@{fa-css-prefix}-beat';
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, cubic-bezier(0.280, 0.840, 0.420, 1))';
}
.@{fa-css-prefix}-fade {
animation-name: ~'@{fa-css-prefix}-fade';
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, cubic-bezier(.4,0,.6,1))';
}
.@{fa-css-prefix}-beat-fade {
animation-name: ~'@{fa-css-prefix}-beat-fade';
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, cubic-bezier(.4,0,.6,1))';
}
.@{fa-css-prefix}-flip {
animation-name: ~'@{fa-css-prefix}-flip';
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, ease-in-out)';
}
.@{fa-css-prefix}-shake {
animation-name: ~'@{fa-css-prefix}-shake';
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, ease-in-out)';
}
.@{fa-css-prefix}-spin {
animation-name: ~'@{fa-css-prefix}-spin';
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 2s)';
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, linear)';
}
.@{fa-css-prefix}-spin-reverse {
--@{fa-css-prefix}-animation-direction: reverse;
}
.@{fa-css-prefix}-pulse,
.@{fa-css-prefix}-spin-pulse {
animation-name: ~'@{fa-css-prefix}-spin';
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, steps(8));';
}
// if agent or operating system prefers reduced motion, disable animations
// see: https://www.smashingmagazine.com/2020/09/design-reduced-motion-sensitivities/
// see: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion
@media (prefers-reduced-motion: reduce) {
.@{fa-css-prefix}-beat,
.@{fa-css-prefix}-bounce,
.@{fa-css-prefix}-fade,
.@{fa-css-prefix}-beat-fade,
.@{fa-css-prefix}-flip,
.@{fa-css-prefix}-pulse,
.@{fa-css-prefix}-shake,
.@{fa-css-prefix}-spin,
.@{fa-css-prefix}-spin-pulse {
animation-delay: -1ms;
animation-duration: 1ms;
animation-iteration-count: 1;
transition-delay: 0s;
transition-duration: 0s;
}
}
@keyframes ~'@{fa-css-prefix}-beat' {
0%, 90% { transform: scale(1); }
45% { transform: ~'scale(var(--@{fa-css-prefix}-beat-scale, 1.25))'; }
}
@keyframes ~'@{fa-css-prefix}-bounce' {
0% { transform: scale(1,1) translateY(0); }
10% { transform: ~'scale(var(--#{$fa-css-prefix}-bounce-start-scale-x, 1.1),var(--#{$fa-css-prefix}-bounce-start-scale-y, 0.9))' translateY(0); }
30% { transform: ~'scale(var(--#{$fa-css-prefix}-bounce-jump-scale-x, 0.9),var(--#{$fa-css-prefix}-bounce-jump-scale-y, 1.1))' ~'translateY(var(--#{$fa-css-prefix}-bounce-height, -0.5em))'; }
50% { transform: ~'scale(var(--#{$fa-css-prefix}-bounce-land-scale-x, 1.05),var(--#{$fa-css-prefix}-bounce-land-scale-y, 0.95))' translateY(0); }
57% { transform: ~'scale(1,1) translateY(var(--#{$fa-css-prefix}-bounce-rebound, -0.125em))'; }
64% { transform: scale(1,1) translateY(0); }
100% { transform: scale(1,1) translateY(0); }
}
@keyframes ~'@{fa-css-prefix}-fade' {
50% { opacity: ~'var(--@{fa-css-prefix}-fade-opacity, 0.4)'; }
}
@keyframes ~'@{fa-css-prefix}-beat-fade' {
0%, 100% {
opacity: ~'var(--@{fa-css-prefix}-beat-fade-opacity, 0.4)';
transform: scale(1);
}
50% {
opacity: 1;
transform: ~'scale(var(--@{fa-css-prefix}-beat-fade-scale, 1.125))';
}
}
@keyframes ~'@{fa-css-prefix}-flip' {
50% {
transform: ~'rotate3d(var(--@{fa-css-prefix}-flip-x, 0), var(--@{fa-css-prefix}-flip-y, 1), var(--@{fa-css-prefix}-flip-z, 0), var(--@{fa-css-prefix}-flip-angle, -180deg))';
}
}
@keyframes ~'@{fa-css-prefix}-shake' {
0% { transform: rotate(-15deg); }
4% { transform: rotate(15deg); }
8%, 24% { transform: rotate(-18deg); }
12%, 28% { transform: rotate(18deg); }
16% { transform: rotate(-22deg); }
20% { transform: rotate(22deg); }
32% { transform: rotate(-12deg); }
36% { transform: rotate(12deg); }
40%, 100% { transform: rotate(0deg); }
}
@keyframes ~'@{fa-css-prefix}-spin' {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -0,0 +1,20 @@
// bordered + pulled icons
// -------------------------
.@{fa-css-prefix}-border {
border-color: ~'var(--@{fa-css-prefix}-border-color, @{fa-border-color})';
border-radius: ~'var(--@{fa-css-prefix}-border-radius, @{fa-border-radius})';
border-style: ~'var(--@{fa-css-prefix}-border-style, @{fa-border-style})';
border-width: ~'var(--@{fa-css-prefix}-border-width, @{fa-border-width})';
padding: ~'var(--@{fa-css-prefix}-border-padding, @{fa-border-padding})';
}
.@{fa-css-prefix}-pull-left {
float: left;
margin-right: ~'var(--@{fa-css-prefix}-pull-margin, @{fa-pull-margin})';
}
.@{fa-css-prefix}-pull-right {
float: right;
margin-left: ~'var(--@{fa-css-prefix}-pull-margin, @{fa-pull-margin})';
}

View File

@ -0,0 +1,38 @@
// base icon class definition
// -------------------------
.@{fa-css-prefix} {
font-family: ~"var(--@{fa-css-prefix}-style-family, '@{fa-style-family}')";
font-weight: ~'var(--@{fa-css-prefix}-style, @{fa-style})';
}
.@{fa-css-prefix},
.fas,
.@{fa-css-prefix}-solid,
.fass,
.@{fa-css-prefix}-sharp,
.far,
.@{fa-css-prefix}-regular,
.fab,
.@{fa-css-prefix}-brands {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
display: ~'var(--@{fa-css-prefix}-display, @{fa-display})';
font-style: normal;
font-variant: normal;
text-rendering: auto;
}
.fas,
.@{fa-css-prefix}-classic,
.@{fa-css-prefix}-solid,
.far,
.@{fa-css-prefix}-regular {
font-family: 'Font Awesome 6 Free';
}
.fab,
.@{fa-css-prefix}-brands {
font-family: 'Font Awesome 6 Brands';
}

View File

@ -0,0 +1,7 @@
// fixed-width icons
// -------------------------
.@{fa-css-prefix}-fw {
text-align: center;
width: @fa-fw-width;
}

View File

@ -0,0 +1,9 @@
// specific icon class definition
// -------------------------
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
readers do not read off random characters that represent icons */
each(.fa-icons(), {
.@{fa-css-prefix}-@{key}::before { content: @value; }
});

View File

@ -0,0 +1,18 @@
// icons in a list
// -------------------------
.@{fa-css-prefix}-ul {
list-style-type: none;
margin-left: ~'var(--@{fa-css-prefix}-li-margin, @{fa-li-margin})';
padding-left: 0;
> li { position: relative; }
}
.@{fa-css-prefix}-li {
left: calc(~'var(--@{fa-css-prefix}-li-width, @{fa-li-width})' * -1);
position: absolute;
text-align: center;
width: ~'var(--@{fa-css-prefix}-li-width, @{fa-li-width})';
line-height: inherit;
}

View File

@ -0,0 +1,78 @@
// mixins
// --------------------------
// base rendering for an icon
.fa-icon() {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
display: inline-block;
font-style: normal;
font-variant: normal;
font-weight: normal;
line-height: 1;
}
// sets relative font-sizing and alignment (in _sizing)
.fa-size(@font-size) {
font-size: (@font-size / @fa-size-scale-base) * 1em; // converts step in sizing scale into an em-based value that's relative to the scale's base
line-height: (1 / @font-size) * 1em; // sets the line-height of the icon back to that of it's parent
vertical-align: ((6 / @font-size) - (3 / 8)) * 1em; // vertically centers the icon taking into account the surrounding text's descender
}
// only display content to screen readers
// see: https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/
// see: https://hugogiraudel.com/2016/10/13/css-hide-and-seek/
.fa-sr-only() {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
// use in conjunction with .sr-only to only display content when it's focused
.fa-sr-only-focusable() {
&:not(:focus) {
.fa-sr-only();
}
}
// sets a specific icon family to use alongside style + icon mixins
.fa-family-classic() {
font-family: 'Font Awesome 6 Free';
}
// convenience mixins for declaring pseudo-elements by CSS variable,
// including all style-specific font properties, and both the ::before
// and ::after elements in the duotone case.
.fa-icon-solid(@fa-var) {
.fa-icon;
.fa-solid;
&::before {
content: @fa-var;
}
}
.fa-icon-regular(@fa-var) {
.fa-icon;
.fa-regular;
&::before {
content: @fa-var;
}
}
.fa-icon-brands(@fa-var) {
.fa-icon;
.fa-brands;
&::before {
content: @fa-var;
}
}

View File

@ -0,0 +1,31 @@
// rotating + flipping icons
// -------------------------
.@{fa-css-prefix}-rotate-90 {
transform: rotate(90deg);
}
.@{fa-css-prefix}-rotate-180 {
transform: rotate(180deg);
}
.@{fa-css-prefix}-rotate-270 {
transform: rotate(270deg);
}
.@{fa-css-prefix}-flip-horizontal {
transform: scale(-1, 1);
}
.@{fa-css-prefix}-flip-vertical {
transform: scale(1, -1);
}
.@{fa-css-prefix}-flip-both,
.@{fa-css-prefix}-flip-horizontal.@{fa-css-prefix}-flip-vertical {
transform: scale(-1, -1);
}
.@{fa-css-prefix}-rotate-by {
transform: rotate(~'var(--@{fa-css-prefix}-rotate-angle, none)');
}

View File

@ -0,0 +1,14 @@
// screen-reader utilities
// -------------------------
// only display content to screen readers
.sr-only,
.@{fa-css-prefix}-sr-only {
.fa-sr-only();
}
// use in conjunction with .sr-only to only display content when it's focused
.sr-only-focusable,
.@{fa-css-prefix}-sr-only-focusable {
.fa-sr-only-focusable();
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
// sizing icons
// -------------------------
// literal magnification scale
.sizes-literal(@factor) when (@factor > 0) {
.sizes-literal((@factor - 1));
.@{fa-css-prefix}-@{factor}x {
font-size: (@factor * 1em);
}
}
.sizes-literal(10);
// step-based scale (with alignment)
each(.fa-sizes(), {
.@{fa-css-prefix}-@{key} {
.fa-size(@value);
}
});

View File

@ -0,0 +1,31 @@
// stacking icons
// -------------------------
.@{fa-css-prefix}-stack {
display: inline-block;
height: 2em;
line-height: 2em;
position: relative;
vertical-align: @fa-stack-vertical-align;
width: @fa-stack-width;
}
.@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x {
left: 0;
position: absolute;
text-align: center;
width: 100%;
z-index: ~'var(--@{fa-css-prefix}-stack-z-index, @{fa-stack-z-index})';
}
.@{fa-css-prefix}-stack-1x {
line-height: inherit;
}
.@{fa-css-prefix}-stack-2x {
font-size: 2em;
}
.@{fa-css-prefix}-inverse {
color: ~'var(--@{fa-css-prefix}-inverse, @{fa-inverse})';
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
@import "_variables.less";
:root, :host {
--@{fa-css-prefix}-style-family-brands: 'Font Awesome 6 Brands';
--@{fa-css-prefix}-font-brands: normal 400 1em/1 'Font Awesome 6 Brands';
}
@font-face {
font-family: 'Font Awesome 6 Brands';
font-style: normal;
font-weight: 400;
font-display: @fa-font-display;
src: url('@{fa-font-path}/fa-brands-400.woff2') format('woff2'),
url('@{fa-font-path}/fa-brands-400.ttf') format('truetype');
}
.fab,
.@{fa-css-prefix}-brands {
font-weight: 400;
}
each(.fa-brand-icons(), {
.@{fa-css-prefix}-@{key}:before { content: @value; }
});

View File

@ -0,0 +1,20 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
// Font Awesome core compile (Web Fonts-based)
// -------------------------
@import "_variables.less";
@import "_mixins.less";
@import "_core.less";
@import "_sizing.less";
@import "_fixed-width.less";
@import "_list.less";
@import "_bordered-pulled.less";
@import "_animated.less";
@import "_rotated-flipped.less";
@import "_stacked.less";
@import "_icons.less";
@import "_screen-reader.less";

View File

@ -0,0 +1,25 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
@import "_variables.less";
:root, :host {
--@{fa-css-prefix}-style-family-classic: '@{fa-style-family}';
--@{fa-css-prefix}-font-regular: normal 400 1em/1 '@{fa-style-family}';
}
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 400;
font-display: @fa-font-display;
src: url('@{fa-font-path}/fa-regular-400.woff2') format('woff2'),
url('@{fa-font-path}/fa-regular-400.ttf') format('truetype');
}
.far,
.@{fa-css-prefix}-regular {
font-weight: 400;
}

Some files were not shown because too many files have changed in this diff Show More