├── .gitignore ├── AUTO_CONFIG.JSON ├── DEPENDENCIES LICENSES ├── GLOBAL_CONFIG.JSON ├── LICENSE ├── MODE.JSON ├── README.md ├── backend ├── configuration.py ├── fast_api │ ├── __init__.py │ ├── fast_api_app.py │ ├── fast_api_data_models.py │ └── fast_api_dependency.py ├── interfaces │ ├── __init__.py │ ├── database.py │ ├── factory.py │ └── models.py ├── mikrotik_test.py ├── requirements.txt ├── server.py └── syslogger │ ├── __init__.py │ ├── bridge.py │ ├── database.py │ ├── engine.py │ └── interface.py ├── demo ├── browse_log.png ├── clients.png ├── edit_engine.png ├── engines.png ├── responisve_1.png ├── responsive_1.jpg ├── responsive_2.jpg ├── responsive_2.png ├── responsive_3.png ├── responsive_4.png └── system.png ├── frontend ├── .eslintrc.json ├── app │ ├── api │ │ ├── textEffect.js │ │ └── validators.js │ ├── auth.js │ ├── backendFunctions.js │ ├── dashboard │ │ ├── browse_log │ │ │ └── page.js │ │ ├── client_management │ │ │ ├── create │ │ │ │ └── page.js │ │ │ ├── delete │ │ │ │ └── page.js │ │ │ ├── edit │ │ │ │ └── page.js │ │ │ ├── layout.js │ │ │ ├── navigation.js │ │ │ └── page.js │ │ ├── components │ │ │ ├── clock.js │ │ │ └── engineStatus.js │ │ ├── context.js │ │ ├── engine_management │ │ │ ├── create │ │ │ │ └── page.js │ │ │ ├── delete │ │ │ │ └── page.js │ │ │ ├── edit │ │ │ │ └── page.js │ │ │ ├── layout.js │ │ │ ├── navigation.js │ │ │ └── page.js │ │ ├── layout.js │ │ ├── license │ │ │ ├── Dependencies.js │ │ │ ├── Permission.js │ │ │ └── page.js │ │ ├── navigation.js │ │ ├── page.js │ │ ├── realtime_status │ │ │ └── page.js │ │ ├── system │ │ │ └── page.js │ │ ├── theme.js │ │ ├── timezone │ │ │ └── page.js │ │ └── user_management │ │ │ ├── account │ │ │ └── page.js │ │ │ ├── change_type │ │ │ └── page.js │ │ │ ├── create │ │ │ └── page.js │ │ │ ├── delete │ │ │ └── page.js │ │ │ ├── layout.js │ │ │ ├── navigation.js │ │ │ └── page.js │ ├── error │ │ └── page.js │ ├── info.js │ ├── layout.js │ ├── login │ │ ├── create │ │ │ └── page.js │ │ ├── layout.js │ │ ├── local.module.css │ │ ├── page.js │ │ └── sharedContext.js │ └── page.js ├── jsconfig.json ├── next.config.js ├── package.json └── public │ ├── DEPENDENCIES LICENSES.txt │ └── logo.svg ├── install.sh └── uninstall.sh /.gitignore: -------------------------------------------------------------------------------- 1 | LOG_DATABASE 2 | **/__pycache__ 3 | **/.idea 4 | **/.venv 5 | **/venv 6 | **/node_modules 7 | **/.next 8 | **/package-lock.json 9 | -------------------------------------------------------------------------------- /AUTO_CONFIG.JSON: -------------------------------------------------------------------------------- 1 | { 2 | "FAST_API_SERVER_IP_WILL_AUTO_GENERATE": "192.168.0.210", 3 | "FAST_API_AUTH_SALT_WILL_AUTO_GENERATE": "WILL_AUTO_GENERATE" 4 | } -------------------------------------------------------------------------------- /GLOBAL_CONFIG.JSON: -------------------------------------------------------------------------------- 1 | { 2 | "FAST_API_SERVER_PORT": 3000, 3 | "ROOT_SAVED_DATA_DIRECTORY": "../LOG_DATABASE", 4 | "INTERFACE_FACTORY_PORT": 3100, 5 | "TOKEN_EXPIRATION": 3600, 6 | "DOWNLOAD_TOKEN_EXPIRATION": 1, 7 | "AUTO_DELETE_DAYS": 180, 8 | "SPACE_PERCENTAGE_TO_STOP": 90, 9 | "FAST_API_ALLOWED_ORIGIN": [ 10 | "*" 11 | ], 12 | "ENGINE_CLASSES": { 13 | "mikrotik": { 14 | "pppoe": "in:", 15 | "router_identity": "\\d{1,2}:\\d{1,2}:\\d{1,2}\\s+(\\w+)", 16 | "user_ip": "(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d+)", 17 | "destination_ip": "->(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d+)", 18 | "nat_ip": "->(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d+)\\)" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Musa Chowdhury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MODE.JSON: -------------------------------------------------------------------------------- 1 | { 2 | "DEVELOPMENT": true 3 | } 4 | -------------------------------------------------------------------------------- /backend/configuration.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | import socket 4 | import string 5 | 6 | CONFIGURATION_FILE_PATH = "../GLOBAL_CONFIG.JSON" 7 | AUTO_CONFIGURATION_FILE_PATH = "../AUTO_CONFIG.JSON" 8 | MODE_FILE_PATH = "../MODE.JSON" 9 | 10 | 11 | def get_local_ip(): 12 | try: 13 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 14 | s.connect(("0.0.0.1", 80)) 15 | local_ip = s.getsockname()[0] 16 | s.close() 17 | return local_ip 18 | except (socket.error, socket.herror, socket.gaierror, socket.timeout): 19 | return "0.0.0.0" 20 | 21 | 22 | with open(CONFIGURATION_FILE_PATH, "r") as configs_json: 23 | server_config = json.load(configs_json) 24 | 25 | with open(AUTO_CONFIGURATION_FILE_PATH, "r") as configs_json: 26 | auto_config = json.load(configs_json) 27 | 28 | with open(MODE_FILE_PATH, "r") as configs_json: 29 | mode = json.load(configs_json) 30 | DEVELOPMENT = mode["DEVELOPMENT"] 31 | 32 | 33 | # note: it is only intended for initial starting up 34 | if (not auto_config.get("FAST_API_AUTH_SALT_WILL_AUTO_GENERATE") or 35 | len(auto_config["FAST_API_AUTH_SALT_WILL_AUTO_GENERATE"]) < 64) and not DEVELOPMENT: 36 | random_salt = ''.join(random.choices(string.ascii_letters + string.digits, k=64)) 37 | auto_config["FAST_API_AUTH_SALT_WILL_AUTO_GENERATE"] = random_salt 38 | # note: it is only intended for initial starting up 39 | 40 | # note: it is intended for every start up 41 | with open(AUTO_CONFIGURATION_FILE_PATH, "w") as updated_auto_config_file: 42 | auto_config["FAST_API_SERVER_IP_WILL_AUTO_GENERATE"] = get_local_ip() 43 | json.dump(auto_config, updated_auto_config_file, indent=4) 44 | # note: it is intended for every start up 45 | 46 | # auto configurations 47 | FAST_API_SERVER_IP_WILL_AUTO_GENERATE = auto_config["FAST_API_SERVER_IP_WILL_AUTO_GENERATE"] 48 | FAST_API_AUTH_SALT_WILL_AUTO_GENERATE = auto_config["FAST_API_AUTH_SALT_WILL_AUTO_GENERATE"] 49 | # auto configurations 50 | 51 | 52 | # reconstructed configurations 53 | FAST_API_ALLOWED_ORIGIN = server_config["FAST_API_ALLOWED_ORIGIN"] 54 | ROOT_SAVED_DATA_DIRECTORY = server_config["ROOT_SAVED_DATA_DIRECTORY"] 55 | FAST_API_SERVER_PORT = server_config["FAST_API_SERVER_PORT"] if not DEVELOPMENT else 2000 56 | INTERFACE_FACTORY_PORT = server_config["INTERFACE_FACTORY_PORT"] if not DEVELOPMENT else 2100 57 | TOKEN_EXPIRATION = server_config["TOKEN_EXPIRATION"] 58 | DOWNLOAD_TOKEN_EXPIRATION = server_config["DOWNLOAD_TOKEN_EXPIRATION"] 59 | AUTO_DELETE_DAYS = server_config["AUTO_DELETE_DAYS"] 60 | SPACE_PERCENTAGE_TO_STOP = server_config["SPACE_PERCENTAGE_TO_STOP"] 61 | ENGINE_CLASSES = server_config["ENGINE_CLASSES"] 62 | # reconstructed configurations 63 | -------------------------------------------------------------------------------- /backend/fast_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/backend/fast_api/__init__.py -------------------------------------------------------------------------------- /backend/fast_api/fast_api_data_models.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | from datetime import datetime 3 | from typing import Annotated, Any, Union, Optional 4 | 5 | import pytz 6 | from pydantic import BaseModel, Field, AfterValidator, BeforeValidator, field_validator, validator 7 | from pydantic_core import PydanticCustomError 8 | from sqlalchemy.orm import DeclarativeBase, mapped_column, Mapped 9 | 10 | 11 | class GenericResponseModel(BaseModel): 12 | status: Annotated[bool | None, Field(description=None)] = None 13 | data: Annotated[dict | None, Field(description=None)] = None 14 | error: Annotated[None | str | list[str], Field(description=None)] = None 15 | 16 | 17 | class UserID(BaseModel): 18 | @staticmethod 19 | def check_if_phone_number_is_correct(number: str): 20 | number = number.strip().replace(" ", "") 21 | if not (0 < len(number) <= 20): 22 | raise PydanticCustomError('invalid_length', 23 | 'phon_number should be less than 20 digits and more than 0 digit', ) 24 | if not number.isdigit(): 25 | raise PydanticCustomError('invalid_phone_number', 26 | 'phone_number should not contain anything but digits 0-9', ) 27 | return number 28 | 29 | @staticmethod 30 | def valid_str(string: str): 31 | string = string.strip() 32 | if len(string) == 0: 33 | raise PydanticCustomError('empty_string', 34 | "after removing leading and trailing whitespace length is zero") 35 | if len(string) < 4 or len(string) > 20: 36 | raise PydanticCustomError( 37 | 'invalid length', 38 | "after removing leading and trailing whitespace length is should be between 4-20") 39 | return string 40 | 41 | phone_number: Annotated[str, AfterValidator(check_if_phone_number_is_correct)] 42 | 43 | 44 | class UserCreationModel(UserID): 45 | password: Annotated[str, Field(max_length=20, min_length=6)] 46 | user_name: Annotated[str, AfterValidator(UserID.valid_str)] 47 | 48 | 49 | class UserUpdateModel(BaseModel): 50 | password: Annotated[str, Field(max_length=20, min_length=6)] 51 | user_name: Annotated[str, AfterValidator(UserID.valid_str)] 52 | 53 | 54 | class UserChangeAdminModel(UserID): 55 | is_admin: Annotated[bool, Field()] 56 | 57 | 58 | class UserLogInModel(UserID): 59 | password: Annotated[str, Field(max_length=20, min_length=6)] 60 | 61 | 62 | class UserCreationModelWithAdmin(UserCreationModel): 63 | is_admin: Annotated[bool, Field()] 64 | user_name: Annotated[str, AfterValidator(UserID.valid_str)] 65 | 66 | 67 | class UserModel(UserID): 68 | is_admin: Annotated[bool, Field()] 69 | user_name: Annotated[str, AfterValidator(UserID.valid_str)] 70 | 71 | 72 | class InterfaceCommunicationModel(BaseModel): 73 | 74 | @staticmethod 75 | def is_port_is_valid(port): 76 | if not (1 <= port <= 65000): 77 | raise PydanticCustomError('invalid_port', 78 | 'port should be between 1 - 65000') 79 | return port 80 | 81 | port: Annotated[int, AfterValidator(is_port_is_valid)] 82 | 83 | 84 | class InterfaceCreateEngine(BaseModel): 85 | @staticmethod 86 | def strip(string: str): 87 | string = string.strip() 88 | if len(string) == 0: 89 | raise PydanticCustomError('empty_string', 90 | "after removing leading and trailing whitespace length is zero") 91 | return string 92 | 93 | @staticmethod 94 | def only_lower(string): 95 | if not all(c.islower() or c.isdigit() or c == '_' for c in string): 96 | raise PydanticCustomError('invalid_character', 97 | "only lowercase letters, digits, and '_' are allowed.") 98 | return string 99 | 100 | engine_class: Annotated[str, AfterValidator(only_lower)] 101 | engine_name: Annotated[str, AfterValidator(only_lower)] 102 | engine_port: Annotated[int, AfterValidator(InterfaceCommunicationModel.is_port_is_valid)] 103 | keep_original_log: Annotated[bool, Field()] 104 | 105 | 106 | class AddClient(InterfaceCommunicationModel): 107 | client: Annotated[str, AfterValidator(InterfaceCreateEngine.only_lower)] 108 | ip: str 109 | 110 | 111 | class DeleteClient(InterfaceCommunicationModel): 112 | client: Annotated[str, AfterValidator(InterfaceCreateEngine.only_lower)] 113 | 114 | 115 | class DeleteEngine(BaseModel): 116 | engine: Annotated[str, AfterValidator(InterfaceCreateEngine.only_lower)] 117 | 118 | 119 | class SetConfiguration(InterfaceCommunicationModel): 120 | @staticmethod 121 | def delay_checker(days: int): 122 | if not (2 <= days <= 10000): 123 | raise PydanticCustomError("invalid auto_delete_days", 124 | "auto_delete_days mush be greater or equal to 2 and less or equal to 10000") 125 | return days 126 | 127 | keep_original_log: Annotated[bool, Field()] 128 | auto_delete_days: Annotated[int, AfterValidator(delay_checker)] 129 | 130 | 131 | class SupportedQuery(InterfaceCommunicationModel): 132 | @staticmethod 133 | def percent_checker(input_string): 134 | # Check if the string contains '%' 135 | if '%' in input_string: 136 | return True 137 | else: 138 | return False 139 | 140 | @staticmethod 141 | def date_validator(date: str): 142 | date = InterfaceCreateEngine.strip(date) 143 | try: 144 | datetime.strptime(date, "%d %B %Y") 145 | return date 146 | except Exception as e: 147 | raise PydanticCustomError('invalid_date', f"invalid date format, {e}") 148 | 149 | client: Annotated[str, InterfaceCreateEngine.only_lower] 150 | date: Annotated[str, AfterValidator(date_validator)] 151 | 152 | 153 | class QueryCount(SupportedQuery): 154 | 155 | @staticmethod 156 | def validate_conditions(conditions: dict): 157 | _temp = {} 158 | if len(conditions.keys()) == 0: 159 | raise PydanticCustomError('empty condition', 160 | f"condition cant be empty") 161 | for key in conditions: 162 | _key = InterfaceCreateEngine.strip(key) 163 | _temp[_key] = conditions[key] 164 | if SupportedQuery.percent_checker(conditions[key]): 165 | raise PydanticCustomError('invalid_value', 166 | f"query value can not have %") 167 | return _temp 168 | 169 | conditions: Annotated[dict[str, str], AfterValidator(validate_conditions)] 170 | 171 | 172 | class DBQuery(QueryCount): 173 | starting: Annotated[int, Field(None, gt=-1)] 174 | limit: Annotated[int, Field(None, gt=0, le=100)] 175 | 176 | 177 | class TimeZone(BaseModel): 178 | @staticmethod 179 | def timezone_validator(zone: str): 180 | zone = InterfaceCreateEngine.strip(zone) 181 | if zone not in list(pytz.common_timezones): 182 | raise PydanticCustomError('invalid timezone', 183 | f"zone is invalid") 184 | return zone 185 | 186 | zone: Annotated[str, AfterValidator(timezone_validator)] 187 | 188 | 189 | class DownloadEncoder(SupportedQuery): 190 | conditions: Optional[dict[str, str]] = None 191 | 192 | @field_validator("conditions") 193 | @classmethod 194 | def validator(cls, value): 195 | return QueryCount.validate_conditions(value) 196 | 197 | model_config = { 198 | "json_schema_extra": { 199 | "examples": [ 200 | { 201 | "port": 0, 202 | "client": "string", 203 | "date": "string", 204 | "conditions": "note: it is optional, if not given, will return token for full database, else for " 205 | "given condition. conditions must be inside a dictionary" 206 | } 207 | ] 208 | } 209 | } 210 | 211 | 212 | class Base(DeclarativeBase): 213 | def __getitem__(self, key): 214 | return getattr(self, key) 215 | 216 | def __setitem__(self, key, value): 217 | setattr(self, key, value) 218 | 219 | def get_dict(self): 220 | result = {} 221 | for column in self.__table__.columns: 222 | column_name = column.name 223 | column_value = getattr(self, column_name) 224 | result[column_name] = column_value 225 | return result 226 | 227 | 228 | class UserOrm(Base): 229 | __tablename__ = 'web_interface_users' 230 | user_name: Mapped[str] = mapped_column(nullable=False) 231 | phone_number: Mapped[str] = mapped_column(nullable=False, primary_key=True, unique=True) 232 | password: Mapped[str] = mapped_column(nullable=False) 233 | is_admin: Mapped[bool] = mapped_column(nullable=False) 234 | 235 | def __repr__(self): 236 | return f"(username: {self.user_name}, phone_number: {self.phone_number}, is_admin: {self.is_admin})" 237 | -------------------------------------------------------------------------------- /backend/fast_api/fast_api_dependency.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | import sqlite3 4 | from datetime import datetime, timedelta 5 | from io import StringIO 6 | from typing import Type, Any 7 | 8 | import jose.constants 9 | import sqlalchemy.exc 10 | from dateutil import parser 11 | from fastapi.responses import StreamingResponse 12 | from jose import jwt 13 | from passlib.context import CryptContext 14 | from sqlalchemy import select, Select, create_engine 15 | from sqlalchemy.orm import sessionmaker, DeclarativeBase 16 | from tzlocal import get_localzone 17 | 18 | from .fast_api_data_models import Base 19 | 20 | 21 | class CryptoAndAuth: 22 | 23 | def __init__(self, private_key: str, token_expire: int = 120): 24 | self._private_key: str = private_key 25 | self._access_token_expiration_time_in_minutes = int(token_expire) 26 | self._hash_manager = CryptContext(schemes=["bcrypt"], deprecated="auto") 27 | if (not isinstance(self._access_token_expiration_time_in_minutes, int) 28 | or self._access_token_expiration_time_in_minutes < 1): 29 | raise Exception("token_expire mus be greater than 1") 30 | 31 | def create_hash_from_plain_text(self, plaintext_password: str): 32 | return self._hash_manager.hash(plaintext_password) 33 | 34 | def compare_hash_to_plain_text(self, plain_text: str, hashed_text: str): 35 | try: 36 | return self._hash_manager.verify(plain_text, hashed_text) 37 | except Exception: 38 | return False 39 | 40 | def return_signed_token(self, pairs: dict): 41 | pairs["expire"] = ((datetime.now(get_localzone()) + 42 | timedelta(minutes=self._access_token_expiration_time_in_minutes)) 43 | .strftime("%Y-%m-%dT%H:%M:%S.%f%z")) 44 | return {"access_token": jwt.encode(pairs, self._private_key, jose.constants.ALGORITHMS.HS256)} 45 | 46 | def verify_signed_token(self, token: str) -> dict | None: 47 | verified = True 48 | result = None 49 | try: 50 | result = jwt.decode(token, self._private_key, jose.constants.ALGORITHMS.HS256, { 51 | "verify_exp": False 52 | }) 53 | except Exception: 54 | verified = False 55 | if result and ( 56 | int(parser.isoparse(result["expire"]).timestamp() 57 | - (datetime.now(get_localzone()).timestamp())) <= 0): 58 | verified = False 59 | result.pop("expire") 60 | return result if verified else None 61 | 62 | 63 | class BackendDatabaseController: 64 | def __init__(self, root_path, base_class: Type["DeclarativeBase"]): 65 | if not isinstance(root_path, str): 66 | raise Exception 67 | if not os.path.exists(os.path.join(root_path)): 68 | os.makedirs(os.path.join(root_path)) 69 | self.__root_path = os.path.join(root_path, "fast_api.db") 70 | if not os.path.exists(self.__root_path): 71 | with open(self.__root_path, 'w') as _: 72 | pass 73 | self.__engine = create_engine(f"sqlite:///{self.__root_path}") 74 | self.__session_maker = sessionmaker(self.__engine) 75 | self.__base_class = base_class 76 | self.__base_class.metadata.create_all(self.__engine) 77 | 78 | @staticmethod 79 | def orm_to_dict(table_instance: Base): 80 | result = {} 81 | for column in table_instance.__table__.columns: 82 | result[column.name] = getattr(table_instance, column.name) 83 | return result 84 | 85 | def inset_orm_to_database(self, row: Base): 86 | temp_session = self.__session_maker() 87 | try: 88 | temp_session.add(row) 89 | temp_session.commit() 90 | temp_session.close() 91 | return True 92 | except sqlalchemy.exc.IntegrityError: 93 | temp_session.close() 94 | return False 95 | 96 | def get_from_database( 97 | self, 98 | table: Type["Base"], 99 | all_or_single_object_or_where_statement: int | float | str | Select | None = None) -> list | None | Any: 100 | temp_session = self.__session_maker() 101 | try: 102 | 103 | if isinstance(all_or_single_object_or_where_statement, Select): 104 | result = temp_session.scalars(all_or_single_object_or_where_statement).all() 105 | elif isinstance(all_or_single_object_or_where_statement, (int, float, str)): 106 | result = temp_session.get(table, all_or_single_object_or_where_statement) 107 | if result is not None: 108 | _result = list() 109 | _result.append(result) 110 | result = _result 111 | 112 | else: 113 | result = list() 114 | else: 115 | result = temp_session.scalars(select(table)).all() 116 | temp_session.close() 117 | return result if len(result) != 0 else None 118 | except Exception: 119 | temp_session.close() 120 | return None 121 | 122 | def update_field_to_database(self, table_instance: Base, updated_key_value_pair: dict): 123 | temp_session = self.__session_maker() 124 | statement = select(type(table_instance)).filter_by(**(self.orm_to_dict(table_instance))) 125 | matched_object: Base = temp_session.scalars(statement).first() 126 | for key in updated_key_value_pair: 127 | matched_object[key] = updated_key_value_pair[key] 128 | temp_session.commit() 129 | temp_session.close() 130 | 131 | def delete_from_database(self, table: Type["Base"], primary_key: str | int | float): 132 | temp_session = self.__session_maker() 133 | try: 134 | target_row = temp_session.get(table, primary_key) 135 | temp_session.delete(target_row) 136 | temp_session.commit() 137 | temp_session.close() 138 | return True 139 | except Exception: 140 | temp_session.close() 141 | return False 142 | 143 | # ONLY FOR DEVELOPMENT PURPOSE 144 | def for_debug_purpose_only_delete_all_data(self): 145 | self.__base_class.metadata.drop_all(self.__engine) 146 | self.__base_class.metadata.create_all(self.__engine) 147 | 148 | # ONLY FOR DEVELOPMENT PURPOSE 149 | 150 | 151 | def database_query(path, sql_statement=None, values=None): 152 | with sqlite3.connect(f'file:{path}?mode=ro', uri=True, check_same_thread=False) as connection: 153 | cursor = connection.cursor() 154 | if sql_statement is None or values is None: 155 | cursor.execute("SELECT * FROM LOG") 156 | else: 157 | cursor.execute(sql_statement, values) 158 | header = [column_name[0] for column_name in cursor.description] 159 | csv_buffer = StringIO() 160 | csv_writer = csv.writer(csv_buffer) 161 | print_head = True 162 | result = False 163 | while True: 164 | rows = cursor.fetchmany(1000) 165 | if not rows: 166 | if result: 167 | break 168 | else: 169 | yield "No Match Found" 170 | break 171 | if print_head: 172 | csv_writer.writerow(header) 173 | print_head = False 174 | 175 | for row in rows: 176 | result = True 177 | csv_writer.writerow(row) 178 | 179 | yield csv_buffer.getvalue() 180 | csv_buffer.truncate(0) 181 | csv_buffer.seek(0) 182 | 183 | 184 | def download_database(name, path, sql_statement=None, values=None): 185 | return StreamingResponse(database_query(path, sql_statement, values), 186 | media_type="text/csv", 187 | headers={"Content-Disposition": f"attachment; filename={name}.csv"}) 188 | -------------------------------------------------------------------------------- /backend/interfaces/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/backend/interfaces/__init__.py -------------------------------------------------------------------------------- /backend/interfaces/database.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Type 4 | 5 | from sqlalchemy import create_engine 6 | from sqlalchemy.orm import Session 7 | 8 | from interfaces.models import Base, Engine, Client 9 | 10 | 11 | class DataBaseController: 12 | def __init__(self, db_path: str, base_class: Type[Base]): 13 | self.__db_path = db_path 14 | self.__engine = create_engine(f"sqlite:///{self.__db_path}") 15 | self.__base_class = base_class 16 | self.__base_class.metadata.create_all(self.__engine) 17 | 18 | def dump_settings(self, configs: dict): 19 | with Session(self.__engine) as session: 20 | for _class in configs: 21 | for engine in configs[_class]: 22 | target_engine = session.query(Engine).filter_by(name=engine["configs"]["engine_name"]).first() 23 | if target_engine: 24 | target_engine.configs = json.dumps(engine["configs"]) 25 | else: 26 | session.add(Engine(name=engine["configs"]["engine_name"], class_type=engine["configs"]["class"], 27 | configs=json.dumps(engine["configs"]))) 28 | session.commit() 29 | target_engine = session.query(Engine).filter_by(name=engine["configs"]["engine_name"]).first() 30 | for client in engine["clients"]: 31 | found = False 32 | for db_client in target_engine.clients: 33 | if db_client.name == client: 34 | found = True 35 | db_client.ip = str(engine["clients"][client][0]) 36 | if not found: 37 | target_engine.clients.append(Client(name=str(client), ip=str(engine["clients"][client][0]))) 38 | session.commit() 39 | 40 | def fetch_settings(self): 41 | _all = [] 42 | with Session(self.__engine) as session: 43 | engines = session.query(Engine).all() 44 | for engine in engines: 45 | temp = { 46 | "configs": json.loads(engine.configs), 47 | "clients": {} 48 | } 49 | for client in engine.clients: 50 | temp["clients"][client.name] = [client.ip, 0] 51 | _all.append(temp) 52 | return _all 53 | 54 | def check_if_client_exist(self, client_name, client_ip): 55 | client_ip = client_ip.strip() 56 | client_name = client_name.strip() 57 | with Session(self.__engine) as session: 58 | clients = session.query(Client).all() 59 | for client in clients: 60 | if client.ip == client_ip or client.name == client_name: 61 | return tuple([client.name, client.ip, client.engine]) 62 | return False 63 | 64 | def check_if_engine_exist(self, engine_name): 65 | engine_name = engine_name.strip() 66 | with Session(self.__engine) as session: 67 | engines = session.query(Engine).all() 68 | for engine in engines: 69 | if engine.name == engine_name: 70 | return tuple([engine.class_type, engine.name]) 71 | return False 72 | 73 | def delete(self, name: str): 74 | with Session(self.__engine) as session: 75 | engine = session.query(Engine).where(Engine.name == name).first() 76 | if engine: 77 | session.delete(engine) 78 | session.commit() 79 | return 80 | raise Exception("engine dont exists in the database") 81 | -------------------------------------------------------------------------------- /backend/interfaces/factory.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import threading 4 | import traceback 5 | from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer 6 | from typing import Callable 7 | 8 | from interfaces.database import DataBaseController 9 | from interfaces.models import Base, Engine, Client 10 | from syslogger.bridge import EngineDatabaseBridge 11 | from syslogger.interface import LogServerWithHTTPInterface 12 | 13 | 14 | class RequestHandlerInterfaceFactory(BaseHTTPRequestHandler): 15 | INTERFACE_CALLBACK: Callable 16 | 17 | def log_message(self, _, *__): 18 | pass 19 | 20 | def do_POST(self): 21 | request = self.receive_the_response_and_validate() 22 | if request is None: 23 | return 24 | try: 25 | result = RequestHandlerInterfaceFactory.INTERFACE_CALLBACK(request=request) 26 | self.send_the_response(result) 27 | return 28 | except Exception as e: 29 | result = f"error, details: [{str(e).lower()}]" 30 | self.send_the_response(result, "error") 31 | 32 | def receive_the_response_and_validate(self) -> None | dict: 33 | content_length = int(self.headers['Content-Length']) 34 | post_data = self.rfile.read(content_length).decode('utf-8') 35 | try: 36 | data_as_dict = json.loads(post_data) 37 | return data_as_dict 38 | except Exception as e: 39 | self.send_response(400) 40 | self.send_header('Content-type', 'application/json') 41 | self.end_headers() 42 | error_message = {"status": "error", "response": f"JSON is invalid, details:[{str(e).lower()}]"} 43 | self.wfile.write(json.dumps(error_message).encode('utf-8')) 44 | return None 45 | 46 | def send_the_response(self, response: dict | str, status: str = "success"): 47 | self.send_response(200) 48 | self.send_header('Content-type', 'application/json') 49 | self.end_headers() 50 | response = json.dumps({"status": status, "response": response}) 51 | self.wfile.write(response.encode('utf-8')) 52 | 53 | 54 | class InterfaceFactory: 55 | def __init__(self, auto_delete_days: int, space_percentage_to_stop: int, server_port, root_path: str, 56 | engine_classes: dict): 57 | self.__auto_delete = auto_delete_days 58 | self.__space_percentage_to_stop = space_percentage_to_stop 59 | self.__port = server_port 60 | self.__root_path = root_path 61 | self.__classes = engine_classes 62 | self.__database_path = None 63 | if not LogServerWithHTTPInterface.is_port_available(server_port): 64 | raise Exception(f"port {self.__port} is unavailable") 65 | try: 66 | if not isinstance(self.__root_path, str): 67 | raise Exception 68 | if os.path.exists(os.path.join(self.__root_path, "factory.db")): 69 | pass 70 | else: 71 | os.makedirs(os.path.join(self.__root_path), exist_ok=True) 72 | with open(os.path.join(self.__root_path, "factory.db"), 'w') as _: 73 | pass 74 | self.__database_path = os.path.join(self.__root_path, "factory.db") 75 | except Exception: 76 | raise Exception(f"invalid path {root_path}") 77 | 78 | if not isinstance(self.__classes, dict): 79 | raise Exception("engine classes must be type of dict") 80 | 81 | self.__running_engines = {} 82 | 83 | for _class in self.__classes: 84 | self.only_lower(_class) 85 | self.__running_engines[_class] = [] 86 | 87 | self.__db_controller = DataBaseController(self.__database_path, Base) 88 | 89 | RequestHandlerInterfaceFactory.INTERFACE_CALLBACK = self.__handel_request 90 | self.__interface_server = ThreadingHTTPServer(("127.0.0.1", self.__port), 91 | RequestHandlerInterfaceFactory) 92 | self.__factory_server_thread = threading.Thread(name="factory http server", 93 | target=self.__interface_server.serve_forever) 94 | self.__factory_server_thread.start() 95 | 96 | self.load_settings() 97 | self.__thread_lock = threading.RLock() 98 | 99 | @staticmethod 100 | def __string_bool_to_value(value: str): 101 | try: 102 | value = value.strip().lower() 103 | if value == "true": 104 | return True 105 | return False 106 | except Exception: 107 | return False 108 | 109 | @staticmethod 110 | def only_lower(string): 111 | if not all(c.islower() or c.isdigit() or c == '_' for c in string): 112 | raise ValueError( 113 | "only lowercase letters, digits, and '_' are allowed.") 114 | 115 | def __create_engine_configs_parser(self, configs: dict): 116 | if isinstance(configs["keep_original_log"], bool): 117 | keep_log = configs["keep_original_log"] 118 | else: 119 | keep_log = self.__string_bool_to_value(configs["keep_original_log"]) 120 | 121 | engine_class = configs["class"] 122 | class_expressions = self.__classes[engine_class] 123 | engine_name = configs["engine_name"] 124 | engine_port = int(configs["engine_port"]) 125 | original_log = keep_log 126 | self.only_lower(engine_class) 127 | self.only_lower(engine_name) 128 | auto_delete_scoped = None 129 | try: 130 | auto_delete_scoped = configs["auto_delete_days"] 131 | except: 132 | pass 133 | engine_configs = { 134 | "configs": { 135 | "class": engine_class, 136 | "engine_name": engine_name, 137 | "engine_port": engine_port, 138 | "keep_original_log": original_log, 139 | "saved_data_root_directory": self.__root_path, 140 | "auto_delete_days": auto_delete_scoped if auto_delete_scoped else self.__auto_delete, 141 | "space_percentage_to_stop": self.__space_percentage_to_stop 142 | }, 143 | "expressions": class_expressions, 144 | 145 | } 146 | return engine_configs 147 | 148 | def create_engine(self, configs: dict, return_instance=False): 149 | 150 | process_config = self.__create_engine_configs_parser(configs) 151 | _class = process_config["configs"]["class"] 152 | name = process_config["configs"]["engine_name"] 153 | if not 4 <= len(name) < 30: 154 | raise Exception("incorrect name length, should be between 4 to 30") 155 | running = self.get_engine_interfaces() 156 | for engine in running[_class]: 157 | if engine["configs"]["engine_name"] == name: 158 | raise Exception("same name already exists") 159 | temp = LogServerWithHTTPInterface(process_config["configs"], process_config["expressions"]) 160 | self.__running_engines[_class].append(temp) 161 | return temp if return_instance else True 162 | 163 | def get_engine_interfaces(self): 164 | classes = {} 165 | for _class in self.__running_engines: 166 | _interfaces = [] 167 | for engine in self.__running_engines[_class]: 168 | if not engine.is_cosed(): 169 | try: 170 | _interfaces.append(engine.get_all_configs()) 171 | except Exception: 172 | traceback.print_exc() 173 | pass 174 | classes[_class] = _interfaces 175 | return classes 176 | 177 | @staticmethod 178 | def get_running_processes(): 179 | return EngineDatabaseBridge.all_thread_or_pprocess(False) 180 | 181 | @staticmethod 182 | def docs(): 183 | return { 184 | "create_engine": { 185 | "class": "str, engine class", 186 | "engine_name": "str, engine name", 187 | "engine_port": "int, engine port", 188 | "keep_original_log": "bool, wherever to keep original log or not" 189 | }, 190 | "running_engines": "only key value, will return all running engines with instance", 191 | "running_processes": "only key value, will return all threads/processes", 192 | "dump_settings": "only key value, will save all engines configs to database, which will use later to " 193 | "restart", 194 | "load_settings": "only key value, will load all saved engines to system", 195 | "delete_engine": {"name": "str, will delete the engine from system, but not from db, for saving the " 196 | "changes call 'dump_settings'"} 197 | 198 | } 199 | 200 | def __handel_request(self, request: dict) -> dict | str | bool: 201 | if len(request.keys()) == 0: 202 | raise Exception(f"no command found") 203 | if len(request.keys()) > 1: 204 | raise Exception(f"multiple command found, only one at a time") 205 | command, = request.keys() 206 | 207 | if command not in self.docs(): 208 | return self.docs() 209 | 210 | if command == "create_engine": 211 | return self.create_engine(request[command]) 212 | elif command == "running_engines": 213 | return self.get_engine_interfaces() 214 | elif command == "running_processes": 215 | return self.get_running_processes() 216 | elif command == "dump_settings": 217 | self.dump_settings() 218 | elif command == "load_settings": 219 | self.load_settings() 220 | elif command == "delete_engine": 221 | self.delete_engine(request[command]) 222 | 223 | def dump_settings(self): 224 | with self.__thread_lock: 225 | running_engines = self.get_engine_interfaces() 226 | self.__db_controller.dump_settings(running_engines) 227 | 228 | def load_settings(self): 229 | setting = self.__db_controller.fetch_settings() 230 | for engine in setting: 231 | _engine = self.create_engine(engine["configs"], True) 232 | for client in engine["clients"]: 233 | _engine.add_client(client, engine["clients"][client][0]) 234 | 235 | def delete_engine(self, info: dict): 236 | name = info["name"].strip() 237 | if not 4 <= len(name) < 30: 238 | raise Exception("incorrect name length, should be between 4 to 30") 239 | found = False 240 | __class = None 241 | target_engine: LogServerWithHTTPInterface | None = None 242 | for _class in self.__running_engines: 243 | for engine in self.__running_engines[_class]: 244 | if engine.get_name() == name: 245 | found = True 246 | target_engine = engine 247 | __class = _class 248 | break 249 | 250 | if found: 251 | self.__running_engines[__class].remove(target_engine) 252 | target_engine.delete_self() 253 | self.__db_controller.delete(name) 254 | else: 255 | raise Exception("engine not found") 256 | -------------------------------------------------------------------------------- /backend/interfaces/models.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from sqlalchemy import ForeignKey 4 | from sqlalchemy.orm import DeclarativeBase 5 | from sqlalchemy.orm import Mapped 6 | from sqlalchemy.orm import mapped_column 7 | from sqlalchemy.orm import relationship 8 | 9 | 10 | class Base(DeclarativeBase): 11 | pass 12 | 13 | 14 | class Engine(Base): 15 | __tablename__ = "engines" 16 | name: Mapped[str] = mapped_column(unique=True, primary_key=True) 17 | configs: Mapped[str] = mapped_column() 18 | class_type: Mapped[str] = mapped_column() 19 | clients: Mapped[List["Client"]] = relationship(cascade="all, delete-orphan") 20 | 21 | def __str__(self): 22 | return f"(name: {self.name}, configs: {self.configs}, clients: {self.clients})" 23 | 24 | def __repr__(self): 25 | return f"(name: {self.name}, configs: {self.configs}, clients: {self.clients})" 26 | 27 | 28 | class Client(Base): 29 | __tablename__ = "clients" 30 | _id: Mapped[int] = mapped_column(unique=True, primary_key=True, autoincrement="auto") 31 | engine: Mapped[str] = mapped_column(ForeignKey("engines.name")) 32 | name: Mapped[str] = mapped_column() 33 | ip: Mapped[str] = mapped_column() 34 | 35 | def __str__(self): 36 | return f"(name: {self.name}, ip: {self.ip}, engine: {self.engine})" 37 | 38 | def __repr__(self): 39 | return f"(name: {self.name}, ip: {self.ip}, engine: {self.engine})" 40 | -------------------------------------------------------------------------------- /backend/mikrotik_test.py: -------------------------------------------------------------------------------- 1 | import random 2 | import socket 3 | import string 4 | import threading 5 | import time 6 | 7 | 8 | def udp_test_client(port: int, delay: float = 0.0001, host: str = "127.0.0.1", ): 9 | def random_log(): 10 | def random_ip(): 11 | ip = ".".join(str(random.randint(0, 255)) for _ in range(4)) 12 | return ip 13 | 14 | def random_name(): 15 | name_length = random.randint(6, 10) 16 | letters_and_numbers = string.ascii_letters + string.digits 17 | _random_name = ''.join(random.choice(letters_and_numbers) for _ in range(name_length)) 18 | return _random_name 19 | 20 | def random_number(): 21 | _random_number = random.randint(100, 999) 22 | return _random_number 23 | 24 | def random_port(): 25 | return random.randint(1024, 49151) 26 | 27 | dummy_string = f'''<{random_number()}> 00:00:00 {random_name()} prerouting: in: out:(unknown 0), connection-state:established,snat proto TCP (ACK,FIN,PSH), {random_ip()}:{random_port()}->{random_ip()}:{random_port()}, NAT ({random_ip()}:{random_port()}->{random_ip()}:{random_port()})->{random_ip()}:{random_port()}, len {random_number()}''' 28 | return dummy_string.encode("utf-8") 29 | 30 | server_address = (host, port) 31 | client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 32 | try: 33 | count = 0 34 | previous = 0 35 | while True: 36 | if delay > 0: 37 | client_socket.sendto(random_log(), server_address) 38 | time.sleep(delay) 39 | else: 40 | client_socket.sendto( 41 | "7812wer345 sd erw345 erf zxv dft 34 534 t dsff qa we21 34 23tg sdf va 23".encode("utf-8"), 42 | server_address) 43 | count += 1 44 | current = time.time() 45 | if current - previous >= 1: 46 | print(f"log send per second: {count}") 47 | count = 0 48 | previous = current 49 | 50 | except KeyboardInterrupt: 51 | print("closed test udp client") 52 | 53 | 54 | if __name__ == "__main__": 55 | # udp_test_client(5600, 0) 56 | threading.Thread(target=udp_test_client(6000, 0)).start() 57 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | # sudo apt update 2 | # sudo apt install -y python3-dev python3-venv build-essential 3 | uvicorn==0.23.2 4 | fastapi==0.103.1 5 | SQLAlchemy==2.0.20 6 | faster-fifo==1.4.5 7 | tzlocal==5.0.1 8 | passlib==1.7.4 9 | pydantic==2.3.0 10 | python-jose[cryptography]==3.3.0 11 | websockets==11.0.3 12 | pymysql==1.1.0 13 | websocket-server==0.6.4 14 | python-multipart==0.0.6 15 | aiohttp==3.9.1 16 | python-dateutil==2.8.2 17 | psutil==5.9.6 18 | pytz==2023.3.post1 -------------------------------------------------------------------------------- /backend/server.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from configuration import (ENGINE_CLASSES, INTERFACE_FACTORY_PORT, ROOT_SAVED_DATA_DIRECTORY, FAST_API_SERVER_PORT, 4 | AUTO_DELETE_DAYS, SPACE_PERCENTAGE_TO_STOP) 5 | from interfaces.factory import InterfaceFactory 6 | 7 | if __name__ == "__main__": 8 | InterfaceFactory(AUTO_DELETE_DAYS, SPACE_PERCENTAGE_TO_STOP, INTERFACE_FACTORY_PORT, ROOT_SAVED_DATA_DIRECTORY, 9 | ENGINE_CLASSES) 10 | uvicorn.run("fast_api.fast_api_app:fast_api_app", host="127.0.0.1", port=FAST_API_SERVER_PORT, reload=True) 11 | -------------------------------------------------------------------------------- /backend/syslogger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/backend/syslogger/__init__.py -------------------------------------------------------------------------------- /demo/browse_log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/demo/browse_log.png -------------------------------------------------------------------------------- /demo/clients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/demo/clients.png -------------------------------------------------------------------------------- /demo/edit_engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/demo/edit_engine.png -------------------------------------------------------------------------------- /demo/engines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/demo/engines.png -------------------------------------------------------------------------------- /demo/responisve_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/demo/responisve_1.png -------------------------------------------------------------------------------- /demo/responsive_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/demo/responsive_1.jpg -------------------------------------------------------------------------------- /demo/responsive_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/demo/responsive_2.jpg -------------------------------------------------------------------------------- /demo/responsive_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/demo/responsive_2.png -------------------------------------------------------------------------------- /demo/responsive_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/demo/responsive_3.png -------------------------------------------------------------------------------- /demo/responsive_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/demo/responsive_4.png -------------------------------------------------------------------------------- /demo/system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MusaChowdhury/Python-Syslog-Server/c1169e7b6d84a08c10b98b32eb6989f822e9ea4b/demo/system.png -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react-hooks/exhaustive-deps": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /frontend/app/api/textEffect.js: -------------------------------------------------------------------------------- 1 | export async function textSequential(text, setter, slower = 5) { 2 | slower = slower * 20; 3 | function sleep(time) { 4 | return new Promise((resolve) => setTimeout(resolve, time)); 5 | } 6 | for (let i = 0; i <= text.length; i++) { 7 | let part = text.substring(0, i); 8 | if (text.length != i) { 9 | setter(part + "|"); 10 | } else { 11 | setter(part); 12 | } 13 | await sleep(1 * slower); 14 | } 15 | } 16 | 17 | export async function textSequentialBack(text, setter, slower = 5) { 18 | slower = slower * 20; 19 | function sleep(time) { 20 | return new Promise((resolve) => setTimeout(resolve, time)); 21 | } 22 | for (let i = text.length; i >= 0; i--) { 23 | let part = text.substring(0, i); 24 | if (text.length != i) { 25 | setter(part + "|"); 26 | } else { 27 | setter(part); 28 | } 29 | await sleep(1 * slower); 30 | } 31 | } 32 | 33 | export async function textRandom(text, setter, slower = 5) { 34 | slower = slower * 20; 35 | function sleep(time) { 36 | return new Promise((resolve) => setTimeout(resolve, time)); 37 | } 38 | function generateRandomArray(range) { 39 | return Array.from({ length: range }, (_, index) => index).sort( 40 | () => Math.random() - 0.5 41 | ); 42 | } 43 | function replaceCharAtPosition(originalString, position, replacementChar) { 44 | if (position < 0 || position >= originalString.length) { 45 | return originalString; 46 | } 47 | const stringArray = originalString.split(""); 48 | stringArray[position] = replacementChar; 49 | const modifiedString = stringArray.join(""); 50 | return modifiedString; 51 | } 52 | function generateRandomString(length) { 53 | const characters = 54 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 55 | let randomString = ""; 56 | 57 | for (let i = 0; i < length; i++) { 58 | const randomIndex = Math.floor(Math.random() * characters.length); 59 | randomString += characters.charAt(randomIndex); 60 | } 61 | 62 | return randomString; 63 | } 64 | 65 | let dummy_text = generateRandomString(text.length); 66 | 67 | for (const number of generateRandomArray(text.length)) { 68 | dummy_text = replaceCharAtPosition(dummy_text, number, text[number]); 69 | setter(dummy_text); 70 | await sleep(1 * slower); 71 | } 72 | } 73 | 74 | export function toTitleCase(str) { 75 | return str.replace(/\b\w/g, (match) => match.toUpperCase()); 76 | } -------------------------------------------------------------------------------- /frontend/app/api/validators.js: -------------------------------------------------------------------------------- 1 | export function isValidNumber(number) { 2 | try { 3 | number = number.trim(); 4 | if (isNaN(Number(number))) { 5 | return "Not Numerical"; 6 | } 7 | if (number.length < 5 || number.length > 18) { 8 | return "Invalid Length, Must Be Between 5-18"; 9 | } 10 | return true; 11 | } catch { 12 | return "Invalid Number"; 13 | } 14 | } 15 | 16 | export function isValidPassword(password) { 17 | try { 18 | if (password.length < 6 || password.length > 18) { 19 | return "Invalid Length, Must Be Between 6-18"; 20 | } 21 | return true; 22 | } catch { 23 | return "Invalid Password"; 24 | } 25 | } 26 | 27 | export function isValidUserName(name) { 28 | try { 29 | name = name.trim(); 30 | if (name.length < 5 || name.length > 16) { 31 | return "Invalid Length, Must Be Between 5-16"; 32 | } 33 | const regex = /^[a-zA-Z0-9\s]+$/; 34 | if (!regex.test(name)) { 35 | return "Name Can Only Contain letters, Numbers and White Space"; 36 | } 37 | return true; 38 | } catch { 39 | return "Invalid Name"; 40 | } 41 | } 42 | 43 | // Generated Using ChatGPT 44 | 45 | export function isValidPort(port) { 46 | const portRegex = 47 | /^(?:[1-9]\d{0,4}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/; 48 | return portRegex.test(port); 49 | } 50 | 51 | export function validateInputText(inputString) { 52 | if (inputString.includes("%")) { 53 | return false; 54 | } else { 55 | return true; 56 | } 57 | } 58 | // Generated Using ChatGPT -------------------------------------------------------------------------------- /frontend/app/auth.js: -------------------------------------------------------------------------------- 1 | import * as backend from "./backendFunctions"; 2 | 3 | export function clearSession() { 4 | localStorage.clear(); 5 | } 6 | 7 | function saveSession(value) { 8 | localStorage.setItem("user", JSON.stringify(value)); 9 | } 10 | 11 | function decodeJWT(token) { 12 | try { 13 | let [header, payload, signature] = token.split("."); 14 | let parsed = JSON.parse(atob(payload)); 15 | parsed["token"] = token; 16 | return parsed; 17 | } catch (error) { 18 | return false; 19 | } 20 | } 21 | 22 | export function isSessionValid() { 23 | try { 24 | let userData = JSON.parse(localStorage.getItem("user")); 25 | let expirationTime = new Date(userData.expire); 26 | let currentTime = new Date(); 27 | return currentTime < expirationTime; 28 | } catch (error) { 29 | return false; 30 | } 31 | } 32 | 33 | export function timeRemainsInSeconds() { 34 | try { 35 | let userData = JSON.parse(localStorage.getItem("user")); 36 | let expirationTime = new Date(userData.expire); 37 | let currentTime = new Date(); 38 | let remain = expirationTime - currentTime; 39 | remain = parseInt((remain / 1000).toFixed(4), 10); 40 | if (remain < 0) { 41 | return 0; 42 | } 43 | if (typeof remain === "number") { 44 | return remain; 45 | } 46 | return 0; 47 | } catch (error) { 48 | return 0; 49 | } 50 | } 51 | 52 | export function getUser() { 53 | try { 54 | let userData = JSON.parse(localStorage.getItem("user")); 55 | return userData; 56 | } catch (error) { 57 | return false; 58 | } 59 | } 60 | 61 | export async function authorizeUser(username, password) { 62 | return await backend 63 | .authorizeUser(username, password) 64 | .then((response) => { 65 | let token = decodeJWT(response.data.access_token); 66 | if (token != false) { 67 | saveSession(token); 68 | return true; 69 | } 70 | throw new Error(); 71 | }) 72 | .catch((error) => { 73 | return false; 74 | }); 75 | } 76 | 77 | export async function renewSession() { 78 | try { 79 | let token = getUser(); 80 | if (token == false) { 81 | return false; 82 | } 83 | token = token["token"]; 84 | return await backend 85 | .renewSession(await getUser().token) 86 | .then((response) => { 87 | let token = decodeJWT(response.data.access_token); 88 | if (token != false) { 89 | saveSession(token); 90 | return true; 91 | } 92 | throw new Error(); 93 | }) 94 | .catch((error) => { 95 | return false; 96 | }); 97 | } catch (error) { 98 | return false; 99 | } 100 | } 101 | 102 | export async function getAllUsers() { 103 | return await backend.getAllUsers(await getUser().token).catch((error) => { 104 | return "error"; 105 | }); 106 | } 107 | 108 | export async function eligibleForAdmin() { 109 | return await backend.eligibleForAdmin(); 110 | } 111 | 112 | export async function alive() { 113 | return await backend.alive(); 114 | } 115 | 116 | export async function createAdmin(username, phone, password) { 117 | return await backend.createAdmin(username, phone, password); 118 | } 119 | 120 | export async function createUser(phone, password, admin, username) { 121 | return backend.createUser( 122 | await getUser().token, 123 | phone, 124 | password, 125 | admin, 126 | username 127 | ); 128 | } 129 | 130 | export async function deleteUser(phone) { 131 | return await backend.deleteUser(await getUser().token, phone); 132 | } 133 | 134 | export async function convertType(phone, admin) { 135 | return await backend.convertType(await getUser().token, phone, admin); 136 | } 137 | 138 | export async function updateSelf(password, username) { 139 | return await backend.updateSelf(await getUser().token, password, username); 140 | } 141 | 142 | export async function getTime() { 143 | return await backend.getTime(); 144 | } 145 | 146 | export async function getEngineLoad(port) { 147 | return await backend.getEngineLoad(await getUser().token, port); 148 | } 149 | 150 | export async function getAllEngine() { 151 | return await backend.getAllEngine(await getUser().token); 152 | } 153 | 154 | export async function getSystemStatus() { 155 | return await backend.getSystemStatus(await getUser().token); 156 | } 157 | 158 | export async function editEngine(keepLog, days, port) { 159 | return await backend.editEngine(await getUser().token, keepLog, days, port); 160 | } 161 | 162 | export async function createEngine(_class, name, port, keepLog) { 163 | return await backend.createEngine( 164 | await getUser().token, 165 | _class, 166 | name, 167 | port, 168 | keepLog 169 | ); 170 | } 171 | 172 | export async function deleteEngine(engineName) { 173 | return await backend.deleteEngine(await getUser().token, engineName); 174 | } 175 | 176 | export async function createClient(port, clientName, clientIp) { 177 | return await backend.createClient( 178 | await getUser().token, 179 | port, 180 | clientName, 181 | clientIp 182 | ); 183 | } 184 | 185 | export async function deleteClient(port, clientName) { 186 | return await backend.deleteClient(await getUser().token, port, clientName); 187 | } 188 | 189 | export async function editClient(port, clientName, clientIp) { 190 | return await backend.editClient( 191 | await getUser().token, 192 | port, 193 | clientName, 194 | clientIp 195 | ); 196 | } 197 | 198 | export async function getHistory(port) { 199 | return await backend.getHistory(await getUser().token, port); 200 | } 201 | 202 | export async function supportedQuery(port, clientName, date) { 203 | return await backend.supportedQuery( 204 | await getUser().token, 205 | port, 206 | clientName, 207 | date 208 | ); 209 | } 210 | export async function query( 211 | port, 212 | clientName, 213 | date, 214 | conditions, 215 | starting, 216 | limit 217 | ) { 218 | return await backend.query( 219 | await getUser().token, 220 | port, 221 | clientName, 222 | date, 223 | conditions, 224 | starting, 225 | limit 226 | ); 227 | } 228 | 229 | export async function queryCount(port, clientName, date, conditions) { 230 | return await backend.queryCount( 231 | await getUser().token, 232 | port, 233 | clientName, 234 | date, 235 | conditions 236 | ); 237 | } 238 | 239 | export async function getDownloadToken( 240 | port, 241 | clientName, 242 | date, 243 | conditions = null 244 | ) { 245 | return await backend.getDownloadToken( 246 | await getUser().token, 247 | port, 248 | clientName, 249 | date, 250 | conditions = conditions 251 | ); 252 | } 253 | 254 | export async function getAllTimeZone() { 255 | return await backend.getAllTimeZone(await getUser().token); 256 | } 257 | 258 | export async function setTImeZone(zone) { 259 | return await backend.setTImeZone(await getUser().token, zone); 260 | } 261 | -------------------------------------------------------------------------------- /frontend/app/dashboard/client_management/create/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createClient, getAllEngine, getUser } from "@/app/auth"; 4 | import { 5 | Typography, 6 | Box, 7 | TextField, 8 | Divider, 9 | Button, 10 | CircularProgress, 11 | Alert, 12 | AlertTitle, 13 | Autocomplete, 14 | } from "@mui/material"; 15 | import { useEffect, useState } from "react"; 16 | import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; 17 | import { useRouter } from "next/navigation"; 18 | 19 | import Dialog from "@mui/material/Dialog"; 20 | import DialogActions from "@mui/material/DialogActions"; 21 | import DialogContent from "@mui/material/DialogContent"; 22 | import DialogContentText from "@mui/material/DialogContentText"; 23 | import DialogTitle from "@mui/material/DialogTitle"; 24 | 25 | const formStyle = { 26 | xs: "100%", 27 | lg: "25%", 28 | }; 29 | 30 | export default function Create(){ 31 | const [enginesUnified, setEnginesUnified] = useState([]); 32 | const [engine, setEngine] = useState(""); 33 | 34 | useEffect(() => { 35 | let _engines = []; 36 | getAllEngine().then((result) => { 37 | 38 | Object.keys(result).forEach((__class) => { 39 | for (let __engine of result[__class]) { 40 | let _engine = {}; 41 | _engine["engine"] = __engine.configs.engine_name; 42 | _engine["interface"] = __engine.others.interface_port; 43 | _engines.push(_engine); 44 | } 45 | }); 46 | setEnginesUnified(_engines); 47 | }); 48 | }, []); 49 | 50 | function getEngines() { 51 | let result = []; 52 | for (let value of enginesUnified) { 53 | result.push(value.engine) 54 | } 55 | return result; 56 | } 57 | 58 | function getEngine(name) { 59 | for (let value of enginesUnified) { 60 | if (name === value.engine) { 61 | return value; 62 | } 63 | } 64 | return null; 65 | } 66 | 67 | const [isAdmin, setIsAdmin] = useState(true); 68 | 69 | useEffect(() => { 70 | setIsAdmin(getUser().is_admin === true); 71 | }, []); 72 | 73 | const router = useRouter(); 74 | 75 | return ( 76 | 86 | {isAdmin ? ( 87 | <> 88 | 100 | { 103 | setEngine(value); 104 | }} 105 | disablePortal 106 | options={getEngines()} 107 | sx={{ width: formStyle, height: "100%" }} 108 | renderInput={(params) => } 109 | /> 110 | 111 | {engine ? :
} 112 | 113 | ) : ( 114 | {}}> 115 | {"Access Denied"} 116 | 117 | 118 | This Page Is Restricted For Users 119 | 120 | 121 | 122 | 130 | 131 | 132 | )} 133 |
134 | ); 135 | }; 136 | 137 | function isValidIP(ipAddress) { 138 | const parts = ipAddress.split("."); 139 | if (parts.length > 4) { 140 | return false; 141 | } 142 | for (const part of parts) { 143 | if (part === "*") { 144 | continue; 145 | } 146 | const num = parseInt(part, 10); 147 | if (isNaN(num) || num < 0 || num > 255) { 148 | return false; 149 | } 150 | if (part !== num.toString()) { 151 | return false; 152 | } 153 | } 154 | return true; 155 | } 156 | 157 | function CreateClient({ engine }) { 158 | const [disableCreate, setDisableCreate] = useState(true); 159 | 160 | const [ip, setIP] = useState("IP"); 161 | const [ipHelper, setIPHelper] = useState(""); 162 | 163 | const [name, setName] = useState("Name"); 164 | const [nameHelper, setNameHelper] = useState(""); 165 | 166 | const [triggerLoading, setTriggerLoading] = useState(false); 167 | 168 | const [validName, setValidName] = useState(false); 169 | const [validIP, setValidIP] = useState(false); 170 | 171 | const [error, setError] = useState(false); 172 | const [msg, setMsg] = useState(false); 173 | 174 | const router = useRouter(); 175 | 176 | useEffect(() => { 177 | if (validName && validIP) { 178 | setDisableCreate(false); 179 | } else { 180 | setDisableCreate(true); 181 | } 182 | }, [validName, validIP]); 183 | 184 | return ( 185 | 195 | {!triggerLoading ? ( 196 | <> 197 | 208 | 214 | {error ? "Error" : "Note"} 215 | {!error ? ( 216 | <> 217 | You Can Not Change Client Name Later 218 | 219 | ) : ( 220 | msg 221 | )} 222 | 223 | 224 | 235 | 236 | Client IP 237 | 238 | { 242 | setError(false); 243 | setIPHelper(""); 244 | let target = value.target.value.trim(); 245 | 246 | try { 247 | if (!isValidIP(target)) { 248 | setIPHelper("IP Is Invalid"); 249 | setValidIP(false); 250 | return; 251 | } 252 | if (target === "0.0.0.0" || target === "127.0.0.1") { 253 | setIPHelper("IP Must Not Point To Self"); 254 | setValidIP(false); 255 | return; 256 | } 257 | } catch {} 258 | value.target.value = target; 259 | setIP(target); 260 | setValidIP(true); 261 | }} 262 | defaultValue={ip} 263 | variant="standard" 264 | helperText={ipHelper} 265 | /> 266 | 267 | 268 | 279 | 280 | Client Name 281 | 282 | { 286 | setError(false); 287 | setNameHelper(""); 288 | let target = value.target.value.trim(); 289 | 290 | try { 291 | if (!/^[a-z0-9_]+$/.test(target)) { 292 | setNameHelper( 293 | "Name Can Only Contain Lowercase Letters, Digits, and '_'" 294 | ); 295 | setValidName(false); 296 | return; 297 | } 298 | if (!(4 <= target.length && target.length <= 16)) { 299 | setNameHelper("Name Length Be Between 4 to 16"); 300 | setValidName(false); 301 | return; 302 | } 303 | } catch {} 304 | value.target.value = target; 305 | setName(target); 306 | setValidName(true); 307 | }} 308 | defaultValue={name} 309 | variant="standard" 310 | helperText={nameHelper} 311 | /> 312 | 313 | 323 | 356 | 357 | 358 | ) : ( 359 | 370 | 371 | 372 | )} 373 | 374 | ); 375 | } 376 | -------------------------------------------------------------------------------- /frontend/app/dashboard/client_management/edit/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { editClient, getAllEngine, getUser } from "@/app/auth"; 4 | import { 5 | Typography, 6 | Box, 7 | TextField, 8 | Divider, 9 | Button, 10 | CircularProgress, 11 | Alert, 12 | AlertTitle, 13 | Autocomplete, 14 | } from "@mui/material"; 15 | import SaveIcon from "@mui/icons-material/Save"; 16 | import { useEffect, useState } from "react"; 17 | import { useRouter } from "next/navigation"; 18 | 19 | import Dialog from "@mui/material/Dialog"; 20 | import DialogActions from "@mui/material/DialogActions"; 21 | import DialogContent from "@mui/material/DialogContent"; 22 | import DialogContentText from "@mui/material/DialogContentText"; 23 | import DialogTitle from "@mui/material/DialogTitle"; 24 | 25 | const formStyle = { 26 | xs: "100%", 27 | lg: "25%", 28 | }; 29 | 30 | export default function Edit() { 31 | const [clientsUnified, setClientsUnified] = useState([]); 32 | const [selectedClient, setSelectedClient] = useState(""); 33 | 34 | useEffect(() => { 35 | let clients = []; 36 | getAllEngine().then((result) => { 37 | Object.keys(result).forEach((__class) => { 38 | for (let engine of result[__class]) { 39 | for (let name in engine.clients) { 40 | let _client = {}; 41 | (_client["name"] = name), 42 | (_client["ip"] = engine.clients[name][0]), 43 | (_client["engine"] = engine.configs.engine_name); 44 | _client["interface"] = engine.others.interface_port; 45 | clients.push(_client); 46 | } 47 | } 48 | }); 49 | setClientsUnified(clients); 50 | }); 51 | }, []); 52 | 53 | function getClients() { 54 | let clients = {}; 55 | for (let value of clientsUnified) { 56 | clients[`${value.name} - ${value.ip}`] = { 57 | name: value.name, 58 | engine: value.engine, 59 | interface: value.interface, 60 | ip: value.ip, 61 | }; 62 | } 63 | return clients; 64 | } 65 | 66 | const [isAdmin, setIsAdmin] = useState(true); 67 | 68 | useEffect(() => { 69 | setIsAdmin(getUser().is_admin === true); 70 | }, []); 71 | 72 | const router = useRouter(); 73 | 74 | return ( 75 | 85 | {isAdmin ? ( 86 | <> 87 | 99 | { 102 | setSelectedClient(value); 103 | }} 104 | disablePortal 105 | options={Object.keys(getClients())} 106 | sx={{ width: formStyle, height: "100%" }} 107 | renderInput={(params) => ( 108 | 109 | )} 110 | /> 111 | 112 | {selectedClient ? ( 113 | 114 | ) : ( 115 |
116 | )} 117 | 118 | ) : ( 119 | {}}> 120 | {"Access Denied"} 121 | 122 | 123 | This Page Is Restricted For Users 124 | 125 | 126 | 127 | 135 | 136 | 137 | )} 138 |
139 | ); 140 | }; 141 | 142 | function isValidIP(ipAddress) { 143 | const parts = ipAddress.split("."); 144 | if (parts.length > 4) { 145 | return false; 146 | } 147 | for (const part of parts) { 148 | if (part === "*") { 149 | continue; 150 | } 151 | const num = parseInt(part, 10); 152 | if (isNaN(num) || num < 0 || num > 255) { 153 | return false; 154 | } 155 | if (part !== num.toString()) { 156 | return false; 157 | } 158 | } 159 | return true; 160 | } 161 | 162 | 163 | function EditClient({ client }) { 164 | const [disableCreate, setDisableCreate] = useState(true); 165 | 166 | const [ip, setIP] = useState("IP"); 167 | const [ipHelper, setIPHelper] = useState(""); 168 | 169 | const [triggerLoading, setTriggerLoading] = useState(false); 170 | 171 | const [validIP, setValidIP] = useState(false); 172 | 173 | const [error, setError] = useState(false); 174 | const [msg, setMsg] = useState(false); 175 | 176 | const router = useRouter(); 177 | 178 | useEffect(() => { 179 | if (validIP) { 180 | setDisableCreate(false); 181 | } else { 182 | setDisableCreate(true); 183 | } 184 | }, [validIP]); 185 | 186 | return ( 187 | 197 | {!triggerLoading ? ( 198 | <> 199 | 210 | 216 | {error ? "Error" : "Note"} 217 | {!error ? ( 218 | <> 219 | Engine: {client.engine}, Client Name:{" "} 220 | {client.name}, Current IP:{" "} 221 | {client.ip} 222 | 223 | ) : ( 224 | msg 225 | )} 226 | 227 | 228 | 239 | 240 | New IP 241 | 242 | { 246 | setError(false); 247 | setIPHelper(""); 248 | let target = value.target.value.trim(); 249 | 250 | try { 251 | if (!isValidIP(target)) { 252 | setIPHelper("IP Is Invalid"); 253 | setValidIP(false); 254 | return; 255 | } 256 | if (target === "0.0.0.0" || target === "127.0.0.1") { 257 | setIPHelper("IP Must Not Point To Self"); 258 | setValidIP(false); 259 | return; 260 | } 261 | if (target === client.ip) { 262 | setIPHelper("Current IP and Provided IP Are Same"); 263 | setValidIP(false); 264 | return; 265 | } 266 | } catch {} 267 | value.target.value = target; 268 | setIP(target); 269 | setValidIP(true); 270 | }} 271 | defaultValue={client.ip} 272 | variant="standard" 273 | helperText={ipHelper} 274 | /> 275 | 276 | 277 | 287 | 314 | 315 | 316 | ) : ( 317 | 328 | 329 | 330 | )} 331 | 332 | ); 333 | } -------------------------------------------------------------------------------- /frontend/app/dashboard/client_management/layout.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Box, Breadcrumbs, Typography, useTheme } from "@mui/material"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | import { navigationIndex } from "./navigation"; 8 | 9 | export default function ManageLayout({ children }) { 10 | const theme = useTheme() 11 | const pathname = usePathname(); 12 | return ( 13 | 24 | 31 | 42 | {navigationIndex.map((index) => { 43 | return ( 44 | 50 | 56 | {index.name} 57 | 58 | 59 | ); 60 | })} 61 | 62 | 63 | 64 | 75 | {children} 76 | 77 | 78 | ); 79 | } -------------------------------------------------------------------------------- /frontend/app/dashboard/client_management/navigation.js: -------------------------------------------------------------------------------- 1 | const rootPath = "/dashboard/client_management"; // this should be based on its relative position 2 | 3 | const navigationIndex = [ 4 | { 5 | name: "View", 6 | routing: rootPath, 7 | }, 8 | { 9 | name: "Create", 10 | routing: rootPath + "/create", 11 | }, 12 | { 13 | name: "Edit", 14 | routing: rootPath + "/edit", 15 | }, 16 | 17 | { 18 | name: "Delete", 19 | routing: rootPath + "/delete", 20 | }, 21 | 22 | , 23 | ]; 24 | 25 | export { navigationIndex }; 26 | -------------------------------------------------------------------------------- /frontend/app/dashboard/client_management/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { getAllEngine } from "@/app/auth"; 3 | import { 4 | Box, 5 | List, 6 | ListItem, 7 | ListItemText, 8 | FormControl, 9 | InputLabel, 10 | Select, 11 | MenuItem, 12 | OutlinedInput, 13 | InputAdornment, 14 | IconButton, 15 | CircularProgress, 16 | useMediaQuery, 17 | Divider, 18 | } from "@mui/material"; 19 | import ImportExportIcon from "@mui/icons-material/ImportExport"; 20 | import SearchIcon from "@mui/icons-material/Search"; 21 | import { useEffect, useState, useRef } from "react"; 22 | import { useTheme } from "@mui/material"; 23 | 24 | export default function View() { 25 | const theme = useTheme(); 26 | const isSmallScreen = useMediaQuery(theme.breakpoints.down("lg")); 27 | const [_class, setClass] = useState(""); 28 | const [classes, setClasses] = useState({}); 29 | 30 | const classChange = (event) => { 31 | setClass(event.target.value); 32 | }; 33 | 34 | const clientsRefs = useRef(new Map()); 35 | const [matchedClients, setMatchedClients] = useState([]); 36 | const [matchedClientsIndex, setMatchedClientsIndex] = useState(-1); 37 | 38 | useEffect(() => { 39 | const allInfoRefresh = setInterval(() => { 40 | const classFiltered = {}; 41 | getAllEngine().then((result) => { 42 | Object.keys(result).forEach((__class) => { 43 | let clients = []; 44 | for (let engine of result[__class]) { 45 | for (let name in engine.clients) { 46 | let _client = {}; 47 | (_client["name"] = name), 48 | (_client["ip"] = engine.clients[name][0]); 49 | _client["size"] = engine.clients[name][1]; 50 | _client["engine"] = engine.configs.engine_name; 51 | _client["class"] = engine.configs.class; 52 | _client["port"] = engine.configs.engine_port; 53 | clients.push(_client); 54 | } 55 | } 56 | classFiltered[__class] = clients; 57 | }); 58 | setClasses(classFiltered); 59 | }); 60 | }, 2000); 61 | return () => { 62 | clearInterval(allInfoRefresh); 63 | }; 64 | }, []); 65 | 66 | function scrollToTarget(target, ref) { 67 | if (ref.current.get(target)) { 68 | ref.current.get(target).scrollIntoView({ 69 | behavior: "smooth", 70 | block: "nearest", 71 | inline: "center", 72 | }); 73 | } 74 | } 75 | 76 | useEffect(() => { 77 | clientsRefs.current.forEach((node, name) => { 78 | if (matchedClients.includes(name)) { 79 | node.style.color = theme.palette.success.light; 80 | } else { 81 | node.style.color = theme.palette.text.primary; 82 | } 83 | }); 84 | }, [matchedClients]); 85 | 86 | useEffect(() => { 87 | scrollToTarget(matchedClients[matchedClientsIndex], clientsRefs); 88 | }, [matchedClientsIndex]); 89 | 90 | return ( 91 | 105 | {Object.entries(classes).length != 0 ? ( 106 | <> 107 | 121 | 131 | Search By Client Name/IP 132 | { 134 | const target = event.target.value.trim().toLowerCase(); 135 | let matched = []; 136 | clientsRefs.current.forEach((node, name) => { 137 | let ip = node.getAttribute("ip"); 138 | if ( 139 | (name.includes(target) || ip.includes(target)) && 140 | target !== "" 141 | ) { 142 | matched.push(name); 143 | } 144 | }); 145 | setMatchedClients(matched); 146 | }} 147 | type="text" 148 | endAdornment={ 149 | 150 | { 152 | if ( 153 | 0 <= matchedClientsIndex && 154 | matchedClientsIndex <= matchedClients.length - 1 155 | ) { 156 | if ( 157 | matchedClientsIndex + 1 >= 158 | matchedClients.length 159 | ) { 160 | setMatchedClientsIndex(0); 161 | } else { 162 | setMatchedClientsIndex(matchedClientsIndex + 1); 163 | } 164 | } else { 165 | setMatchedClientsIndex(0); 166 | } 167 | }} 168 | edge="end" 169 | > 170 | {matchedClients.length == 0 ? ( 171 | 172 | ) : ( 173 | 174 | )} 175 | 176 | 177 | } 178 | label="Search By Client Name/IP" 179 | /> 180 | 181 | 190 | 191 | {classes[_class] != undefined && classes[_class].length != 0 192 | ? `Client Found: ${classes[_class].length}` 193 | : "Class"} 194 | 195 | 214 | 215 | 216 | 225 | {classes[_class] != undefined ? ( 226 | 235 | {classes[_class].length != 0 ? ( 236 | classes[_class].map((client) => { 237 | return ( 238 | 239 | 248 | { 254 | clientsRefs.current.set(client.name, element); 255 | }} 256 | /> 257 | 262 | 263 | 268 | {!isSmallScreen ? ( 269 | <> 270 | 275 | 280 | 285 | 286 | ) : ( 287 |
288 | )} 289 |
290 | 291 |
292 | ); 293 | }) 294 | ) : ( 295 | 296 | 297 | 298 | )} 299 |
300 | ) : ( 301 |
302 | )} 303 |
304 | 305 | ) : ( 306 | 307 | )} 308 |
309 | ); 310 | } 311 | -------------------------------------------------------------------------------- /frontend/app/dashboard/components/clock.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useContext } from "react"; 3 | import { Paper, Typography } from "@mui/material"; 4 | import { TimeContext } from "../context"; 5 | 6 | function Clock() { 7 | const { currentTimeFull, currentDate } = useContext(TimeContext); 8 | return ( 9 | <> 10 | 26 | {currentDate} 27 | {currentTimeFull} 28 | 29 | 30 | ); 31 | } 32 | 33 | export default Clock; 34 | -------------------------------------------------------------------------------- /frontend/app/dashboard/components/engineStatus.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { LineChart } from "@mui/x-charts/LineChart"; 3 | import { useEffect, useState, useContext, useMemo } from "react"; 4 | import { ErrorContext, TimeContext } from "../context"; 5 | import { 6 | Typography, 7 | useMediaQuery, 8 | useTheme, 9 | Divider, 10 | Box, 11 | } from "@mui/material"; 12 | import { getEngineLoad } from "@/app/auth"; 13 | 14 | function addToList(setList, data, string = true, limit = 5) { 15 | setList((oldList) => { 16 | let newList = [...oldList]; 17 | 18 | if (newList.length > limit) { 19 | newList.shift(); 20 | } 21 | 22 | newList.push(string ? String(data) : parseInt(data)); 23 | 24 | return newList; 25 | }); 26 | } 27 | 28 | export default function EngineStatus({ engineInfo }) { 29 | const interfacePort = engineInfo.others.interface_port; 30 | const engineName = engineInfo.configs.engine_name; 31 | const engineClass = engineInfo.configs.class; 32 | const enginePort = engineInfo.configs.engine_port; 33 | const setEngineError = useContext(ErrorContext); 34 | const { currentTime } = useContext(TimeContext); 35 | const [incoming, setIncoming] = useState([0, 0, 0, 0, 0]); 36 | const [rejecting, setRejecting] = useState([0, 0, 0, 0, 0]); 37 | const [processing, setProcessing] = useState([0, 0, 0, 0, 0]); 38 | const [saving, setSaving] = useState([0, 0, 0, 0, 0]); 39 | const [times, setTimes] = useState(["", "", "", "", ""]); 40 | const [diskFull, setDiskFull] = useState(false); 41 | const [interfaceError, setInterfaceError] = useState(false); 42 | 43 | useEffect(() => { 44 | addToList(setTimes, currentTime); 45 | }, [currentTime]); 46 | 47 | useEffect(() => { 48 | const timer = setInterval(async () => { 49 | let load = await getEngineLoad(interfacePort).then((result) => { 50 | if (Object.keys(result).length === 0) { 51 | return "error"; 52 | } 53 | return result; 54 | }); 55 | if (load === "error") { 56 | setInterfaceError(true); 57 | clearInterval(timer); 58 | } 59 | if (load.server_receiving_log === "no space") { 60 | setDiskFull(true); 61 | setEngineError("Disk Space Full, All Engine Is Paused"); 62 | } else { 63 | setDiskFull(false); 64 | if (diskFull === true) { 65 | setEngineError(""); 66 | } 67 | } 68 | addToList(setProcessing, load.realtime_processing_client_log, false); 69 | addToList(setRejecting, load.realtime_rejecting_unknown_ip, false); 70 | addToList(setSaving, load.realtime_saving, false); 71 | addToList(setIncoming, load.server_receiving_log, false); 72 | }, 1000); 73 | return () => clearInterval(timer); 74 | }, []); 75 | 76 | const theme = useTheme(); 77 | const isSmallScreen = useMediaQuery(theme.breakpoints.down("lg")); 78 | 79 | const chats = useMemo(() => [ 80 | { 81 | data: incoming, 82 | label: "Incoming", 83 | area: true, 84 | showMark: false, 85 | color: "#fa9632", 86 | hidden: false, 87 | }, 88 | { 89 | data: rejecting, 90 | label: "Rejecting", 91 | area: true, 92 | showMark: false, 93 | color: "red", 94 | hidden: true, 95 | }, 96 | { 97 | data: processing, 98 | label: "Processing", 99 | area: true, 100 | showMark: false, 101 | color: "blue", 102 | hidden: false, 103 | }, 104 | { 105 | data: saving, 106 | label: "Saving", 107 | area: true, 108 | showMark: false, 109 | color: "green", 110 | hidden: true, 111 | }, 112 | ], [incoming, rejecting, processing, saving]); 113 | 114 | return ( 115 | <> 116 | {diskFull ? ( 117 | 129 | 130 | Engine Is Paused 131 | 132 | 133 | Engine Name: {engineName} 134 | 135 | Engine Class: {engineClass} 136 | 137 | Engine Port: {enginePort} 138 | 139 | ) : ( 140 |
141 | )} 142 | {!interfaceError && !diskFull ? ( 143 | 152 | 162 | 172 | 182 | {`Engine: ${engineName}, Port: ${enginePort}, Class: ${engineClass}`} 183 | 184 | 194 | 195 | 196 | 209 | {chats.map((config) => ( 210 | 216 | ))} 217 | 218 | 219 | ) : ( 220 | 221 | )} 222 | 223 | ); 224 | } 225 | 226 | function PerformanceChart({ config, data, smallScreen }) { 227 | const hideOnSmallScreen = config.hidden; 228 | 229 | const small = () => smallScreen && hideOnSmallScreen; 230 | 231 | const updatedConfig = useMemo(() => ({ 232 | ...config, 233 | label: `${String(config.label)} [Current: ${config.data[config.data.length - 1]}]` 234 | }), [config, config.data]); 235 | 236 | return ( 237 | 244 | {smallScreen && hideOnSmallScreen ? ( 245 | 246 | ) : ( 247 | 260 | )} 261 | 262 | ); 263 | } 264 | -------------------------------------------------------------------------------- /frontend/app/dashboard/context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export const TimeContext = createContext(null); 4 | export const ErrorContext = createContext(null); -------------------------------------------------------------------------------- /frontend/app/dashboard/engine_management/edit/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { editEngine, getAllEngine, getUser } from "@/app/auth"; 4 | import { 5 | Typography, 6 | Box, 7 | Autocomplete, 8 | TextField, 9 | FormControl, 10 | InputLabel, 11 | Select, 12 | MenuItem, 13 | Switch, 14 | Divider, 15 | Button, 16 | CircularProgress, 17 | Alert, 18 | AlertTitle, 19 | } from "@mui/material"; 20 | import { useEffect, useState } from "react"; 21 | import SaveIcon from "@mui/icons-material/Save"; 22 | 23 | import Dialog from "@mui/material/Dialog"; 24 | import DialogActions from "@mui/material/DialogActions"; 25 | import DialogContent from "@mui/material/DialogContent"; 26 | import DialogContentText from "@mui/material/DialogContentText"; 27 | import DialogTitle from "@mui/material/DialogTitle"; 28 | import { useRouter } from "next/navigation"; 29 | 30 | const formStyle = { 31 | xs: "100%", 32 | lg: "25%", 33 | }; 34 | 35 | export default function Edit() { 36 | const router = useRouter(); 37 | const [classes, setClasses] = useState({}); 38 | const [_class, setClass] = useState(""); 39 | const [engines, setEngines] = useState([]); 40 | const [engine, setEngine] = useState(null); 41 | 42 | useEffect(() => { 43 | getAllEngine().then((result) => { 44 | setClasses(result); 45 | }); 46 | }, []); 47 | 48 | useEffect(() => { 49 | let _engines = []; 50 | for (const engine in classes[_class]) { 51 | let _engine = {}; 52 | let name = classes[_class][engine]["configs"]["engine_name"]; 53 | let port = classes[_class][engine]["others"]["interface_port"]; 54 | 55 | let keep_log = classes[_class][engine]["configs"]["keep_original_log"]; 56 | let days = classes[_class][engine]["configs"]["auto_delete_days"]; 57 | _engine["days"] = days; 58 | _engine["keep_log"] = keep_log; 59 | 60 | _engine["label"] = name; 61 | _engine["port"] = port; 62 | 63 | _engines.push(_engine); 64 | } 65 | setEngines(_engines); 66 | }, [_class]); 67 | 68 | const [isAdmin, setIsAdmin] = useState(true); 69 | 70 | useEffect(() => { 71 | setIsAdmin(getUser().is_admin === true); 72 | }, []); 73 | 74 | return ( 75 | 85 | {isAdmin ? ( 86 | <> 87 | 99 | 105 | Class 106 | 121 | 122 | { 124 | setEngine(value); 125 | }} 126 | disablePortal 127 | options={engines} 128 | sx={{ width: formStyle, height: "100%" }} 129 | renderInput={(params) => } 130 | /> 131 | 132 | {engine != undefined ? :
} 133 | 134 | ) : ( 135 | {}}> 136 | {"Access Denied"} 137 | 138 | 139 | This Page Is Restricted For Users 140 | 141 | 142 | 143 | 151 | 152 | 153 | )} 154 |
155 | ); 156 | }; 157 | 158 | function EditEngine({ configs }) { 159 | const port = configs.port; 160 | const [keepLog, setKeepLog] = useState(configs.keep_log); 161 | const [days, setDays] = useState(configs.days); 162 | const [disableSave, setDisableSave] = useState(true); 163 | const [daysHelper, setDaysHelper] = useState(""); 164 | const [triggerLoading, setTriggerLoading] = useState(false); 165 | const router = useRouter(); 166 | const [failedToChange, setFailedToChange] = useState(false); 167 | const [validDays, setValidDays] = useState(true); 168 | 169 | return ( 170 | 180 | {}}> 181 | {"Failed To Edit"} 182 | 183 | 184 | Failed To Save Changes for Unknown Reason. App Will Now Redirect To 185 | Manage Engine. Check If Everything Is Working Or Not. Contact 186 | Developer If The Issue Persist 187 | 188 | 189 | 190 | 198 | 199 | 200 | {!triggerLoading ? ( 201 | <> 202 | 212 | 218 | Warning 219 | {`Do Not Change Settings While The Engine Is Receiving The Client's `} 220 | Data.

Also, Do Not Change Settings Frequently.

221 | Doing Either Of These May Result In Permanent 222 | Data Loss. 223 |
224 |
225 | 236 | 237 | Keep Original Log 238 | 239 | { 242 | setKeepLog(event.target.checked); 243 | if (/^[1-9]\d*$/.test(days) && 2 <= days && days <= 2000) { 244 | setDisableSave(false); 245 | } else { 246 | setDisableSave(true); 247 | } 248 | if ( 249 | configs.keep_log == event.target.checked && 250 | configs.days == days 251 | ) { 252 | setDisableSave(true); 253 | } 254 | }} 255 | /> 256 | 257 | 258 | 269 | 270 | Auto Delete After Days 271 | 272 | { 276 | setDisableSave(true); 277 | setDaysHelper(""); 278 | let target = value.target.value.trim(); 279 | value.target.value = target; 280 | setDays(target); 281 | try { 282 | if (!/^[1-9]\d*$/.test(target)) { 283 | setDaysHelper("Days Must Be Integer"); 284 | setValidDays(false); 285 | return; 286 | } 287 | if (!(2 <= target && target <= 2000)) { 288 | setDaysHelper("Days Must Be Between 2 to 2000"); 289 | setValidDays(false); 290 | return; 291 | } 292 | } catch {} 293 | setDisableSave(false); 294 | setValidDays(true); 295 | if (target == configs.days && configs.keep_log == keepLog) { 296 | setDisableSave(true); 297 | } 298 | }} 299 | defaultValue={days} 300 | variant="standard" 301 | helperText={daysHelper} 302 | /> 303 | 304 | 314 | 334 | 335 | 336 | ) : ( 337 | 348 | 349 | 350 | )} 351 |
352 | ); 353 | } 354 | -------------------------------------------------------------------------------- /frontend/app/dashboard/engine_management/layout.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Box, Breadcrumbs, Typography, useTheme } from "@mui/material"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | import { navigationIndex } from "./navigation"; 8 | 9 | export default function ManageLayout({ children }) { 10 | const theme = useTheme() 11 | const pathname = usePathname(); 12 | return ( 13 | 24 | 31 | 42 | {navigationIndex.map((index) => { 43 | return ( 44 | 50 | 56 | {index.name} 57 | 58 | 59 | ); 60 | })} 61 | 62 | 63 | 64 | 75 | {children} 76 | 77 | 78 | ); 79 | } -------------------------------------------------------------------------------- /frontend/app/dashboard/engine_management/navigation.js: -------------------------------------------------------------------------------- 1 | const rootPath = "/dashboard/engine_management"; // this should be based on its relative position 2 | 3 | const navigationIndex = [ 4 | { 5 | name: "View", 6 | routing: rootPath, 7 | }, 8 | { 9 | name: "Create", 10 | routing: rootPath + "/create", 11 | }, 12 | { 13 | name: "Edit", 14 | routing: rootPath + "/edit", 15 | }, 16 | 17 | { 18 | name: "Delete", 19 | routing: rootPath + "/delete", 20 | }, 21 | 22 | , 23 | ]; 24 | 25 | export { navigationIndex }; -------------------------------------------------------------------------------- /frontend/app/dashboard/license/Dependencies.js: -------------------------------------------------------------------------------- 1 | import { Box, Link, Typography } from "@mui/material"; 2 | 3 | export function Dependencies() { 4 | return ( 5 | <> 6 | 13 | 14 | 15 | {`Fast Logger Utilizes Dependencies Mentioned In 'DEPENDENCIES 16 | LICENSES' File Which You Will Need To Download Using Link Below.`} 17 | 18 | 19 | If You Do Not Agree, Please Discontinue The Use Of The Software 20 | Immediately. 21 | 22 | 23 | 29 | 33 | {`Download 'DEPENDENCIES LICENSES'`} 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /frontend/app/dashboard/license/Permission.js: -------------------------------------------------------------------------------- 1 | import { Box, Typography } from "@mui/material"; 2 | 3 | export function Permission() { 4 | return ( 5 | <> 6 | 13 | 14 | 15 | {`By Using This Software, You Are By Default Agreeing With This Software Licensing and Its Dependencies' License Agreement.`} 16 | 17 | 18 | {`If You Don't Agree With This or Any of Its Software Agreement, Stop 19 | Using the Software Immediately and Remove It From the System.`} 20 | 21 | 22 | 29 | 30 |

31 | {`Copyright (c) 2023 Musa Chowdhury`} 32 |

33 |

34 | {`Permission is hereby granted, free of charge, to any person 35 | obtaining a copy of this software and associated documentation files 36 | (the "Software"), to deal in the Software without restriction, 37 | including without limitation the rights to use, copy, modify, merge, 38 | publish, distribute, sublicense, and/or sell copies of the Software, 39 | and to permit persons to whom the Software is furnished to do so, 40 | subject to the following conditions:`} 41 |

42 |

43 | 44 | {`The above copyright notice and this permission notice shall be 45 | included in all copies or substantial portions of the Software.`} 46 |

47 |

48 | 49 | {`THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 50 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 51 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 52 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 53 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 54 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 55 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 56 | SOFTWARE.`} 57 |
58 | 59 | {`The Mentioned License Applies Only to the Fast Logger Software 60 | Itself. It Does Not Apply to the Dependencies or Any Related Systems 61 | It Relies On.`} 62 |

63 |

64 |
65 |
66 |
67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /frontend/app/dashboard/license/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Box from "@mui/material/Box"; 3 | import Tab from "@mui/material/Tab"; 4 | import TabContext from "@mui/lab/TabContext"; 5 | import TabList from "@mui/lab/TabList"; 6 | import TabPanel from "@mui/lab/TabPanel"; 7 | import { useState } from "react"; 8 | import { Permission } from "./Permission"; 9 | import { Dependencies } from "./Dependencies"; 10 | 11 | export default function License() { 12 | 13 | const [value, setValue] = useState("1"); 14 | 15 | const handleChange = (event, newValue) => { 16 | setValue(newValue); 17 | }; 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /frontend/app/dashboard/navigation.js: -------------------------------------------------------------------------------- 1 | import RouterIcon from "@mui/icons-material/Router"; 2 | import ManageAccountsIcon from "@mui/icons-material/ManageAccounts"; 3 | import SvgIcon from "@mui/material/SvgIcon"; 4 | import ManageSearchIcon from "@mui/icons-material/ManageSearch"; 5 | import AvTimerIcon from "@mui/icons-material/AvTimer"; 6 | import QueryStatsIcon from "@mui/icons-material/QueryStats"; 7 | import PersonalVideoIcon from "@mui/icons-material/PersonalVideo"; 8 | import CopyrightIcon from "@mui/icons-material/Copyright"; 9 | import AccessTimeIcon from "@mui/icons-material/AccessTime"; 10 | const EngineIcon = (props) => { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | const rootPath = "/dashboard"; // this should be based on its relative position 19 | 20 | const navigationIndex = [ 21 | { 22 | name: "System", 23 | icon: PersonalVideoIcon, 24 | routing: rootPath + "/system", 25 | }, 26 | { 27 | name: "divider", 28 | number: 1, 29 | }, 30 | { 31 | name: "Dashboard", 32 | icon: QueryStatsIcon, 33 | routing: rootPath, 34 | }, 35 | { 36 | name: "Engine Management", 37 | icon: EngineIcon, 38 | routing: rootPath + "/engine_management", 39 | }, 40 | { 41 | name: "Client Management", 42 | icon: RouterIcon, 43 | routing: rootPath + "/client_management", 44 | }, 45 | { 46 | name: "Realtime Status", 47 | icon: AvTimerIcon, 48 | routing: rootPath + "/realtime_status", 49 | }, 50 | { 51 | name: "Browse Log", 52 | icon: ManageSearchIcon, 53 | routing: rootPath + "/browse_log", 54 | }, 55 | { 56 | name: "divider", 57 | number: 2, 58 | }, 59 | { 60 | name: "User Management", 61 | icon: ManageAccountsIcon, 62 | routing: rootPath + "/user_management", 63 | }, 64 | 65 | { 66 | name: "divider", 67 | number: 3, 68 | }, 69 | { 70 | name: "Time Zone", 71 | icon: AccessTimeIcon, 72 | routing: rootPath + "/timezone", 73 | }, 74 | { 75 | name: "divider", 76 | number: 4, 77 | }, 78 | { 79 | name: "Copyright", 80 | icon: CopyrightIcon, 81 | routing: rootPath + "/license", 82 | }, 83 | ]; 84 | 85 | export { navigationIndex }; 86 | -------------------------------------------------------------------------------- /frontend/app/dashboard/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Typography, Box, CircularProgress } from "@mui/material"; 4 | import EngineStatus from "./components/engineStatus"; 5 | import { getAllEngine } from "@/app/auth"; 6 | import { 7 | useEffect, 8 | useState, 9 | useRef, 10 | } from "react"; 11 | import SearchIcon from "@mui/icons-material/Search"; 12 | import Input from "@mui/material/Input"; 13 | import InputLabel from "@mui/material/InputLabel"; 14 | import InputAdornment from "@mui/material/InputAdornment"; 15 | import FormControl from "@mui/material/FormControl"; 16 | import ImportExportIcon from "@mui/icons-material/ImportExport"; 17 | import { useTheme } from "@mui/material/styles"; 18 | 19 | export default function DashBoard() { 20 | const [engines, setEngines] = useState([]); 21 | const [loading, setLoading] = useState(true); 22 | 23 | useEffect(() => { 24 | getAllEngine().then((result) => { 25 | let unifiedEngines = []; 26 | if (result != "error") { 27 | Object.keys(result).forEach((key) => { 28 | const classBasedEngines = result[key]; 29 | classBasedEngines.map((engine_conf) => { 30 | unifiedEngines.push(engine_conf); 31 | }); 32 | }); 33 | } 34 | let readyToPush = [...engines, ...unifiedEngines]; 35 | setEngines(readyToPush); 36 | setLoading(false); 37 | }); 38 | }, []); 39 | 40 | return loading ? : ; 41 | } 42 | 43 | function StatusHolder({ engines }) { 44 | const theme = useTheme(); 45 | const refEngines = useRef([]); 46 | 47 | const [matched, setMatched] = useState([]); 48 | 49 | const [scrollIndex, setScrollIndex] = useState(0); 50 | 51 | useEffect(() => { 52 | setScrollIndex(matched.length == 0 ? 0 : matched.length - 1); 53 | }, [matched]); 54 | 55 | function scrollToElement() { 56 | setScrollIndex( 57 | scrollIndex - 1 < 0 58 | ? matched.length == 0 59 | ? 0 60 | : matched.length - 1 61 | : scrollIndex - 1 62 | ); 63 | if (matched[scrollIndex]) { 64 | matched[scrollIndex].scrollIntoView({ 65 | behavior: "smooth", 66 | }); 67 | } 68 | } 69 | 70 | return ( 71 | 80 | 91 | 92 | Search By Engine Name or Class 93 | { 95 | let target = value.target.value.toLowerCase().trim(); 96 | let matched_ = []; 97 | refEngines.current.map((node) => { 98 | let engineClass = node.getAttribute("_class").toLowerCase(); 99 | let name = node.getAttribute("name").toLowerCase(); 100 | if ( 101 | (engineClass.includes(target) || name.includes(target)) && 102 | target.length != 0 103 | ) { 104 | node.style.borderColor = theme.palette.success.light; 105 | matched_.push(node); 106 | } else { 107 | node.style.borderColor = theme.palette.primary.main; 108 | } 109 | }); 110 | setMatched(matched_); 111 | }} 112 | startAdornment={ 113 | 118 | {matched.length == 0 ? : } 119 | 120 | } 121 | /> 122 | 123 | 124 | 135 | {engines.length > 0 ? ( 136 | engines.map((engine, index) => { 137 | return ( 138 | { 140 | if (node) { 141 | refEngines.current.push(node); 142 | } 143 | }} 144 | key={engine.configs.engine_name} 145 | name={engine.configs.engine_name} 146 | _class={engine.configs.class} 147 | sx={{ 148 | display: "flex", 149 | width: "100%", 150 | height: "60%", 151 | p: 1, 152 | border: 1, 153 | borderRadius: 2, 154 | borderColor: "primary.main", 155 | }} 156 | > 157 | 158 | 159 | ); 160 | }) 161 | ) : ( 162 | Currently No Engine Is Running 163 | )} 164 | 165 | 166 | ); 167 | } 168 | -------------------------------------------------------------------------------- /frontend/app/dashboard/realtime_status/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { getAllEngine, getUser } from "@/app/auth"; 4 | import { getWebsocketURL } from "@/app/backendFunctions"; 5 | import { 6 | Box, 7 | TextField, 8 | FormControl, 9 | InputLabel, 10 | Select, 11 | MenuItem, 12 | CircularProgress, 13 | Alert, 14 | Autocomplete, 15 | List, 16 | ListItem, 17 | ListItemText, 18 | } from "@mui/material"; 19 | import { useEffect, useRef, useState } from "react"; 20 | 21 | const formStyle = { 22 | xs: "100%", 23 | lg: "25%", 24 | }; 25 | 26 | export default function RealtimeStatus() { 27 | const [enginesUnified, setEnginesUnified] = useState([]); 28 | const [engine, setEngine] = useState(null); 29 | const [type, setType] = useState("saved"); 30 | useEffect(() => { 31 | let _engines = []; 32 | getAllEngine().then((result) => { 33 | Object.keys(result).forEach((__class) => { 34 | for (let __engine of result[__class]) { 35 | let _engine = {}; 36 | _engine["engine"] = __engine.configs.engine_name; 37 | _engine["log"] = __engine.others.internal_status_websocket_port; 38 | _engine["saved"] = __engine.others.saved_data_websocket_port; 39 | _engine["pause"] = undefined; 40 | _engines.push(_engine); 41 | } 42 | }); 43 | setEnginesUnified(_engines); 44 | }); 45 | }, []); 46 | 47 | function getEngines() { 48 | let result = []; 49 | for (let value of enginesUnified) { 50 | if (!result.includes(value.engine)) { 51 | result.push(value.engine); 52 | } 53 | } 54 | return result; 55 | } 56 | 57 | function getEngine(name) { 58 | for (let value of enginesUnified) { 59 | if (name === value.engine) { 60 | return value; 61 | } 62 | } 63 | return null; 64 | } 65 | 66 | return ( 67 | 78 | 79 | Realtime Status Will Show Only Some But Sequential Entries Provided By 80 | Server.

81 | All of The Information Is Directly Coming From Engine With Some Delay 82 | Usually Around Few Seconds.

83 | It Was Done To Improve Performance, As Synchronization With Engine With 84 | Exact Second Precision Will Case Unnecessary Load On Server. 85 |

86 | This Feature Is Intended For Advance Use Case/Debugging 87 |
88 | 100 | { 103 | setEngine(value); 104 | }} 105 | disablePortal 106 | options={getEngines()} 107 | sx={{ width: formStyle, height: "100%" }} 108 | renderInput={(params) => } 109 | /> 110 | {engine ? ( 111 | 112 | Type 113 | 124 | 125 | ) : ( 126 |
127 | )} 128 |
129 | 130 | {type && engine ? ( 131 | 132 | ) : ( 133 |
134 | )} 135 |
136 | ); 137 | } 138 | 139 | function formatValues(jsonString) { 140 | try { 141 | const dictionary = JSON.parse(jsonString); 142 | const keyValuePairs = Object.entries(dictionary).map( 143 | ([key, value]) => `${key}: ${value}` 144 | ); 145 | return keyValuePairs.join("; "); 146 | } catch { 147 | return ""; 148 | } 149 | } 150 | 151 | function addToList(setList, data, limit = 50) { 152 | setList((oldList) => { 153 | let newList = [...oldList]; 154 | 155 | if (newList.length > limit) { 156 | newList.shift(); 157 | } 158 | 159 | newList.push(data); 160 | 161 | return newList; 162 | }); 163 | } 164 | 165 | function Realtime({ port }) { 166 | const [triggerLoading, setTriggerLoading] = useState(true); 167 | const [log, setLog] = useState([]); 168 | const listContainer = useRef(null); 169 | const [url, setUrl] = useState(""); 170 | useEffect(() => { 171 | getWebsocketURL().then((url) => { 172 | const socketUrl = `${url}?interface=${port}&token=${getUser().token}`; 173 | setUrl(socketUrl); 174 | }); 175 | }, [port]); 176 | 177 | useEffect(() => { 178 | if (url.length == 0) { 179 | return; 180 | } 181 | const socket = new WebSocket(url); 182 | 183 | socket.addEventListener("open", (event) => { 184 | setTriggerLoading(false); 185 | setLog([]); 186 | }); 187 | 188 | socket.addEventListener("message", (event) => { 189 | addToList(setLog, formatValues(event.data)); 190 | }); 191 | 192 | socket.addEventListener("close", (event) => { 193 | if (port != undefined) { 194 | setTriggerLoading(true); 195 | } else { 196 | setTriggerLoading(false); 197 | } 198 | }); 199 | 200 | return () => { 201 | socket.close(); 202 | }; 203 | }, [url]); 204 | 205 | useEffect(() => { 206 | scrollToBottom(); 207 | }, [log]); 208 | 209 | const scrollToBottom = () => { 210 | if (listContainer.current) { 211 | listContainer.current.scrollTop = listContainer.current.scrollHeight; 212 | } 213 | }; 214 | 215 | return ( 216 | <> 217 | 227 | {!triggerLoading ? ( 228 | 238 | {log.length != 0 ? ( 239 | log.map((item, index) => { 240 | return ( 241 | 242 | 243 | 244 | ); 245 | }) 246 | ) : ( 247 | 248 | 249 | 250 | )} 251 | 252 | ) : ( 253 | 263 | 264 | 265 | )} 266 | 267 | 268 | ); 269 | } 270 | -------------------------------------------------------------------------------- /frontend/app/dashboard/system/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Box, Paper, Typography, useTheme } from "@mui/material"; 3 | import { LineChart } from "@mui/x-charts"; 4 | import { PieChart, pieArcLabelClasses } from "@mui/x-charts/PieChart"; 5 | import { TimeContext } from "../context"; 6 | import { useContext, useEffect, useState } from "react"; 7 | import { getSystemStatus } from "@/app/auth"; 8 | 9 | const defaultContainerStyle = { 10 | height: "49%", 11 | width: { 12 | lg: "49%", 13 | xs: "100%", 14 | }, 15 | display: "flex", 16 | }; 17 | 18 | const cpuContainerStyle = { 19 | height: "49%", 20 | width: { 21 | lg: "98.5%", 22 | xs: "100%", 23 | }, 24 | display: "flex", 25 | }; 26 | 27 | function addToList(setList, data, string = true, limit = 5) { 28 | setList((oldList) => { 29 | let newList = [...oldList]; 30 | 31 | if (newList.length > limit) { 32 | newList.shift(); 33 | } 34 | 35 | newList.push(string ? String(data) : parseInt(data)); 36 | 37 | return newList; 38 | }); 39 | } 40 | 41 | export default function SystemStatus() { 42 | const [systemStatus, setSystemStatus] = useState({}); 43 | 44 | const systemChecker = () => { 45 | getSystemStatus().then((response) => { 46 | setSystemStatus(response); 47 | }); 48 | }; 49 | 50 | useEffect(() => { 51 | const systemCheckerInterval = setInterval(systemChecker, 1000); 52 | return () => { 53 | clearInterval(systemCheckerInterval); 54 | }; 55 | }, []); 56 | 57 | return ( 58 | 71 | 72 | {systemStatus.disk ? ( 73 | 74 | ) : ( 75 | 76 | )} 77 | 78 | 79 | {systemStatus.ram ? ( 80 | 81 | ) : ( 82 | 83 | )} 84 | 85 | 86 | {systemStatus.cpu ? ( 87 | 88 | ) : ( 89 | 90 | )} 91 | 92 | 93 | ); 94 | } 95 | 96 | function DiskStatus({ diskInfo }) { 97 | const theme = useTheme(); 98 | const systemGB = diskInfo.system; 99 | const systemP = diskInfo.percent_system; 100 | const logGB = diskInfo.log; 101 | const logP = diskInfo.percent_log; 102 | const freeGB = diskInfo.free; 103 | const freeP = diskInfo.percent_free; 104 | const total = diskInfo.total; 105 | const diskStatus = [ 106 | { 107 | value: systemGB, 108 | label: "System", 109 | percent: systemP, 110 | color: theme.palette.primary.main, 111 | }, 112 | { 113 | value: logGB, 114 | label: "Log", 115 | percent: logP, 116 | color: theme.palette.error.main, 117 | }, 118 | { 119 | value: freeGB, 120 | label: "Free", 121 | percent: freeP, 122 | color: theme.palette.success.main, 123 | }, 124 | ]; 125 | return ( 126 | 135 | 136 | Total Disk Space: {total} GB 137 | 138 | `${item.percent}%`, 147 | arcLabelMinAngle: 40, 148 | data: diskStatus, 149 | valueFormatter: (value) => { 150 | return `${value.value} GB`; 151 | }, 152 | highlightScope: { faded: "global", highlighted: "item" }, 153 | faded: { innerRadius: 30, additionalRadius: -30, color: "gray" }, 154 | }, 155 | ]} 156 | /> 157 | 158 | ); 159 | } 160 | 161 | function CpuStatus({ cpuInfo }) { 162 | const theme = useTheme(); 163 | const name = cpuInfo.details; 164 | const percent = cpuInfo.percent; 165 | const core = cpuInfo.core; 166 | const { currentTime } = useContext(TimeContext); 167 | 168 | const [times, setTimes] = useState(["", "", "", "", ""]); 169 | const [percents, setPercents] = useState([0, 0, 0, 0, 0]); 170 | 171 | useEffect(() => { 172 | addToList(setPercents, percent, false, 10); 173 | addToList(setTimes, currentTime, true, 10); 174 | }, [currentTime]); 175 | 176 | return ( 177 | 186 | 198 | 199 | Logical Core: {core} 200 | 201 | 202 | 211 | ,  212 | 213 | 222 | Name: {name} 223 | 224 | 225 | 226 | { 234 | return `${value} %`; 235 | }, 236 | color: theme.palette.secondary.main, 237 | }, 238 | ]} 239 | yAxis={[ 240 | { 241 | min: 0, 242 | max: 100, 243 | valueFormatter: (value) => { 244 | return `${value} %`; 245 | }, 246 | }, 247 | ]} 248 | xAxis={[{ scaleType: "point", data: times }]} 249 | sx={{ 250 | ".MuiLineElement-root": { 251 | display: "none", 252 | }, 253 | }} 254 | /> 255 | 256 | ); 257 | } 258 | 259 | function RamStatus({ ramInfo }) { 260 | const theme = useTheme(); 261 | const size = ramInfo.total; 262 | const percent = ramInfo.percent; 263 | 264 | const { currentTime } = useContext(TimeContext); 265 | 266 | const [times, setTimes] = useState(["", "", "", "", ""]); 267 | const [percents, setPercents] = useState([0, 0, 0, 0, 0]); 268 | 269 | useEffect(() => { 270 | addToList(setPercents, percent, false); 271 | addToList(setTimes, currentTime); 272 | }, [currentTime]); 273 | 274 | return ( 275 | 284 | 295 | 296 | Ram Size: {size} GB 297 | 298 | 299 | 300 | { 308 | return `${value} %`; 309 | }, 310 | color: theme.palette.warning.main, 311 | }, 312 | ]} 313 | yAxis={[ 314 | { 315 | min: 0, 316 | max: 100, 317 | valueFormatter: (value) => { 318 | return `${value} %`; 319 | }, 320 | }, 321 | ]} 322 | xAxis={[{ scaleType: "point", data: times }]} 323 | sx={{ 324 | ".MuiLineElement-root": { 325 | display: "none", 326 | }, 327 | }} 328 | /> 329 | 330 | ); 331 | } -------------------------------------------------------------------------------- /frontend/app/dashboard/theme.js: -------------------------------------------------------------------------------- 1 | export function getPallet(dark = false) { 2 | if (!dark) { 3 | return { 4 | palette: { 5 | mode: "light", 6 | primary: { 7 | main: "#60a5fa", 8 | light: "#054696", 9 | }, 10 | }, 11 | }; 12 | } else { 13 | return { 14 | palette: { 15 | mode: "dark", 16 | }, 17 | }; 18 | } 19 | } -------------------------------------------------------------------------------- /frontend/app/dashboard/timezone/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { getAllTimeZone, getUser, setTImeZone } from "@/app/auth"; 3 | import { 4 | Box, 5 | Alert, 6 | Autocomplete, 7 | TextField, 8 | Typography, 9 | Button, 10 | DialogTitle, 11 | DialogContentText, 12 | Dialog, 13 | DialogContent, 14 | DialogActions, 15 | } from "@mui/material"; 16 | import { useRouter } from "next/navigation"; 17 | import { useEffect, useState } from "react"; 18 | 19 | export default function TimeZone() { 20 | const router = useRouter(); 21 | const [currentZone, setCurrentZone] = useState(""); 22 | const [zones, setZones] = useState([]); 23 | const [selected, setSelected] = useState(null); 24 | const [disable, setDisable] = useState(false); 25 | const [error, setError] = useState(false); 26 | useEffect(() => { 27 | getAllTimeZone().then((result) => { 28 | setCurrentZone(result.data.current_zone); 29 | setZones([...result.data.all_zones]); 30 | }); 31 | }, []); 32 | const [isAdmin, setIsAdmin] = useState(true); 33 | 34 | useEffect(() => { 35 | setIsAdmin(getUser().is_admin === true); 36 | }, []); 37 | return ( 38 | <> 39 | {isAdmin == false ? ( 40 | {}}> 41 | {"Access Denied"} 42 | 43 | 44 | This Page Is Restricted For Users 45 | 46 | 47 | 48 | 56 | 57 | 58 | ) : ( 59 | 69 | {error == true ? ( 70 | 71 | {`Failed To Change Zone For Unknown Reason`} 72 | 73 | ) : ( 74 | 75 | {`Changing The Time Zone Of The System Can Lead To Unexpected Behavior, 76 | And It's Generally Not Recommended To Do So Frequently, Especially 77 | During Runtime. `}{" "} 78 | Otherwise, May Cause Data Loss. 79 |

80 | {`It Is Better To Change The Zone When The System Is Not 81 | On High Load Or Engines Are Idle.`} 82 |

83 | 84 | The Full System Will Restart If Time Zone Changes. 85 | 86 |
87 | )} 88 | 101 | Current Time Zone: {" "} 102 | {currentZone} 103 | 104 | { 109 | setSelected(_selected); 110 | setDisable(false); 111 | }} 112 | sx={{ 113 | pl: { 114 | xs: 0, 115 | lg: 2, 116 | }, 117 | width: { 118 | xs: "100%", 119 | lg: "30%", 120 | }, 121 | }} 122 | renderInput={(params) => ( 123 | 124 | )} 125 | /> 126 | 136 | 158 | 159 |
160 | )} 161 | 162 | ); 163 | } 164 | -------------------------------------------------------------------------------- /frontend/app/dashboard/user_management/account/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | clearSession, 5 | getUser, 6 | updateSelf, 7 | } from "@/app/auth"; 8 | import { 9 | Typography, 10 | Box, 11 | TextField, 12 | Divider, 13 | Button, 14 | CircularProgress, 15 | Alert, 16 | AlertTitle, 17 | } from "@mui/material"; 18 | 19 | import SaveIcon from "@mui/icons-material/Save"; 20 | import { useEffect, useState } from "react"; 21 | import { useRouter } from "next/navigation"; 22 | 23 | import { 24 | isValidPassword, 25 | isValidUserName, 26 | } from "@/app/api/validators"; 27 | import { toTitleCase } from "@/app/api/textEffect"; 28 | 29 | export default function Account(){ 30 | return ( 31 | 41 | 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | function CreateUser() { 60 | const [disableCreate, setDisableCreate] = useState(true); 61 | 62 | const [name, setName] = useState("Name"); 63 | const [nameHelper, setNameHelper] = useState(""); 64 | const [validName, setValidName] = useState(false); 65 | 66 | const [password, setPassword] = useState("Password"); 67 | const [passwordHelper, setPasswordHelper] = useState(""); 68 | const [validPassword, setValidPassword] = useState(false); 69 | 70 | const [triggerLoading, setTriggerLoading] = useState(false); 71 | 72 | const [error, setError] = useState(false); 73 | 74 | const [msg, setMsg] = useState(false); 75 | 76 | const router = useRouter(); 77 | 78 | let self = getUser(); 79 | 80 | useEffect(() => { 81 | if (validName && validPassword) { 82 | setDisableCreate(false); 83 | } else { 84 | setDisableCreate(true); 85 | } 86 | }, [validName, validPassword]); 87 | 88 | return ( 89 | 99 | {!triggerLoading ? ( 100 | <> 101 | 112 | 118 | {error ? "Error" : "Note"} 119 | {!error ? ( 120 | <> 121 | Account Information 122 |

123 | Phone Number: {self.phone_number} 124 |

125 | User Name: {self.user_name} 126 |

127 | Type: {self.is_admin === true ? "Admin" : "User"} 128 |

129 | 130 | If You Do Not Want To Change Password, Give The Current 131 | Password 132 | 133 |

134 | 135 | If You Do Not Want To Change Username, Give The Current 136 | UserName 137 | 138 | 139 | ) : ( 140 | msg 141 | )} 142 |
143 |
144 | 155 | 156 | Password 157 | 158 | { 162 | setError(false); 163 | setPasswordHelper(""); 164 | let target = value.target.value; 165 | 166 | try { 167 | if (isValidPassword(target) !== true) { 168 | setPasswordHelper(isValidPassword(target)); 169 | setValidPassword(false); 170 | return; 171 | } 172 | } catch {} 173 | value.target.value = target; 174 | setPassword(target); 175 | setValidPassword(true); 176 | }} 177 | defaultValue={password} 178 | variant="standard" 179 | helperText={passwordHelper} 180 | /> 181 | 182 | 183 | 194 | 195 | User Name 196 | 197 | { 201 | setError(false); 202 | setNameHelper(""); 203 | if (value.target.value[value.target.value.length - 1] === " ") { 204 | value.target.value = value.target.value.trim() + " "; 205 | } else { 206 | value.target.value = value.target.value.trim(); 207 | } 208 | let target = toTitleCase(value.target.value); 209 | value.target.value = target; 210 | try { 211 | if (isValidUserName(target) !== true) { 212 | setNameHelper(isValidUserName(target)); 213 | setValidName(false); 214 | return; 215 | } 216 | } catch {} 217 | setName(target); 218 | setValidName(true); 219 | }} 220 | defaultValue={self.user_name} 221 | variant="standard" 222 | helperText={nameHelper} 223 | /> 224 | 225 | 235 | 257 | 258 | 259 | ) : ( 260 | 271 | 272 | 273 | )} 274 |
275 | ); 276 | } -------------------------------------------------------------------------------- /frontend/app/dashboard/user_management/create/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createUser, getUser } from "@/app/auth"; 4 | import { 5 | Typography, 6 | Box, 7 | TextField, 8 | Switch, 9 | Divider, 10 | Button, 11 | CircularProgress, 12 | Alert, 13 | AlertTitle, 14 | } from "@mui/material"; 15 | import { useEffect, useState } from "react"; 16 | import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; 17 | import { useRouter } from "next/navigation"; 18 | 19 | import Dialog from "@mui/material/Dialog"; 20 | import DialogActions from "@mui/material/DialogActions"; 21 | import DialogContent from "@mui/material/DialogContent"; 22 | import DialogContentText from "@mui/material/DialogContentText"; 23 | import DialogTitle from "@mui/material/DialogTitle"; 24 | import { 25 | isValidNumber, 26 | isValidPassword, 27 | isValidUserName, 28 | } from "@/app/api/validators"; 29 | import { toTitleCase } from "@/app/api/textEffect"; 30 | 31 | export default function Create(){ 32 | const [isAdmin, setIsAdmin] = useState(true); 33 | 34 | useEffect(() => { 35 | setIsAdmin(getUser().is_admin === true); 36 | }, []); 37 | 38 | const router = useRouter(); 39 | 40 | return ( 41 | 51 | {isAdmin ? ( 52 | <> 53 | 65 | 66 | 67 | 68 | ) : ( 69 | {}}> 70 | {"Access Denied"} 71 | 72 | 73 | This Page Is Restricted For Users 74 | 75 | 76 | 77 | 85 | 86 | 87 | )} 88 | 89 | ); 90 | }; 91 | 92 | function CreateUser() { 93 | const [disableCreate, setDisableCreate] = useState(true); 94 | 95 | const [phone, setPhone] = useState("Phone"); 96 | const [phoneHelper, setPhoneHelper] = useState(""); 97 | const [validPhone, setValidPhone] = useState(false); 98 | 99 | const [name, setName] = useState("Name"); 100 | const [nameHelper, setNameHelper] = useState(""); 101 | const [validName, setValidName] = useState(false); 102 | 103 | const [password, setPassword] = useState("Password"); 104 | const [passwordHelper, setPasswordHelper] = useState(""); 105 | const [validPassword, setValidPassword] = useState(false); 106 | 107 | const [triggerLoading, setTriggerLoading] = useState(false); 108 | 109 | const [admin, setAdmin] = useState(false); 110 | 111 | const [error, setError] = useState(false); 112 | const [msg, setMsg] = useState(false); 113 | 114 | const router = useRouter(); 115 | 116 | useEffect(() => { 117 | if (validName && validPhone && validPassword) { 118 | setDisableCreate(false); 119 | } else { 120 | setDisableCreate(true); 121 | } 122 | }, [validName, validPhone, validPassword]); 123 | 124 | return ( 125 | 135 | {!triggerLoading ? ( 136 | <> 137 | 148 | 154 | {error ? "Error" : "Note"} 155 | {!error ? ( 156 | <> 157 | Phone Number Can Not Be{" "} 158 | Changed Later. Admin Can Control Every Part 159 | of The System 160 | 161 | ) : ( 162 | msg 163 | )} 164 | 165 | 166 | 177 | 178 | Phone 179 | 180 | { 184 | setError(false); 185 | setPhoneHelper(""); 186 | let target = value.target.value.trim(); 187 | 188 | try { 189 | if (isValidNumber(target) !== true) { 190 | setPhoneHelper(isValidNumber(target)); 191 | setValidPhone(false); 192 | return; 193 | } 194 | } catch {} 195 | value.target.value = target; 196 | setPhone(target); 197 | setValidPhone(true); 198 | }} 199 | defaultValue={phone} 200 | variant="standard" 201 | helperText={phoneHelper} 202 | /> 203 | 204 | 205 | 216 | 217 | Password 218 | 219 | { 223 | setError(false); 224 | setPasswordHelper(""); 225 | let target = value.target.value; 226 | 227 | try { 228 | if (isValidPassword(target) !== true) { 229 | setPasswordHelper(isValidPassword(target)); 230 | setValidPassword(false); 231 | return; 232 | } 233 | } catch {} 234 | value.target.value = target; 235 | setPassword(target); 236 | setValidPassword(true); 237 | }} 238 | defaultValue={password} 239 | variant="standard" 240 | helperText={passwordHelper} 241 | /> 242 | 243 | 244 | 255 | 256 | User Name 257 | 258 | { 262 | setError(false); 263 | setNameHelper(""); 264 | if (value.target.value[value.target.value.length - 1] === " ") { 265 | value.target.value = value.target.value.trim() + " "; 266 | } else { 267 | value.target.value = value.target.value.trim(); 268 | } 269 | let target = toTitleCase(value.target.value); 270 | value.target.value = target; 271 | try { 272 | if (isValidUserName(target) !== true) { 273 | setNameHelper(isValidUserName(target)); 274 | setValidName(false); 275 | return; 276 | } 277 | } catch {} 278 | setName(target); 279 | setValidName(true); 280 | }} 281 | defaultValue={name} 282 | variant="standard" 283 | helperText={nameHelper} 284 | /> 285 | 286 | 287 | 298 | 299 | Admin 300 | 301 | { 304 | setAdmin(event.target.value); 305 | }} 306 | /> 307 | 308 | 318 | 345 | 346 | 347 | ) : ( 348 | 359 | 360 | 361 | )} 362 | 363 | ); 364 | } 365 | -------------------------------------------------------------------------------- /frontend/app/dashboard/user_management/layout.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Box, Breadcrumbs, Typography, useTheme } from "@mui/material"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | import { navigationIndex } from "./navigation"; 8 | 9 | export default function ManageLayout({ children }) { 10 | const theme = useTheme() 11 | const pathname = usePathname(); 12 | return ( 13 | 24 | 31 | 42 | {navigationIndex.map((index) => { 43 | return ( 44 | 50 | 56 | {index.name} 57 | 58 | 59 | ); 60 | })} 61 | 62 | 63 | 64 | 75 | {children} 76 | 77 | 78 | ); 79 | } -------------------------------------------------------------------------------- /frontend/app/dashboard/user_management/navigation.js: -------------------------------------------------------------------------------- 1 | const rootPath = "/dashboard/user_management"; // this should be based on its relative position 2 | 3 | const navigationIndex = [ 4 | { 5 | name: "View", 6 | routing: rootPath, 7 | }, 8 | { 9 | name: "Create", 10 | routing: rootPath + "/create", 11 | }, 12 | { 13 | name: "Account", 14 | routing: rootPath + "/account", 15 | }, 16 | { 17 | name: "Change Type", 18 | routing: rootPath + "/change_type", 19 | }, 20 | 21 | { 22 | name: "Delete", 23 | routing: rootPath + "/delete", 24 | }, 25 | 26 | , 27 | ]; 28 | 29 | export { navigationIndex }; -------------------------------------------------------------------------------- /frontend/app/dashboard/user_management/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { getAllUsers } from "@/app/auth"; 3 | import { 4 | Box, 5 | List, 6 | ListItem, 7 | ListItemText, 8 | FormControl, 9 | InputLabel, 10 | Select, 11 | MenuItem, 12 | OutlinedInput, 13 | InputAdornment, 14 | IconButton, 15 | CircularProgress, 16 | useMediaQuery, 17 | Divider, 18 | } from "@mui/material"; 19 | import ImportExportIcon from "@mui/icons-material/ImportExport"; 20 | import SearchIcon from "@mui/icons-material/Search"; 21 | import { useEffect, useState, useRef } from "react"; 22 | import { useTheme } from "@mui/material"; 23 | 24 | export default function View() { 25 | const theme = useTheme(); 26 | const isSmallScreen = useMediaQuery(theme.breakpoints.down("lg")); 27 | 28 | const [category, setCategory] = useState(""); 29 | 30 | const [users, setUsers] = useState([]); 31 | const usersRefs = useRef(new Map()); 32 | 33 | const [matchedUsers, setMatchedUsers] = useState([]); 34 | const [matchedUserIndex, setMatchedUserIndex] = useState(-1); 35 | 36 | function getUsers(type) { 37 | let _users = []; 38 | for (let user of users) { 39 | if (type === "admin") { 40 | if (user.is_admin === true) { 41 | _users.push(user); 42 | } 43 | } else if (type === "user") { 44 | if (user.is_admin !== true) { 45 | _users.push(user); 46 | } 47 | } else if (type === "all") { 48 | _users.push(user); 49 | } 50 | } 51 | return _users; 52 | } 53 | 54 | const categoryChange = (event) => { 55 | setCategory(event.target.value); 56 | }; 57 | 58 | useEffect(() => { 59 | const allInfoRefresh = setInterval(() => { 60 | const _users = []; 61 | getAllUsers().then((result) => { 62 | result.data.users.forEach((user) => { 63 | _users.push(user); 64 | }); 65 | setUsers(_users); 66 | }); 67 | }, 2000); 68 | return () => { 69 | clearInterval(allInfoRefresh); 70 | }; 71 | }, []); 72 | 73 | function scrollToTarget(target, ref) { 74 | if (ref.current.get(target)) { 75 | ref.current.get(target).scrollIntoView({ 76 | behavior: "smooth", 77 | block: "nearest", 78 | inline: "center", 79 | }); 80 | } 81 | } 82 | 83 | useEffect(() => { 84 | usersRefs.current.forEach((node, name) => { 85 | if (matchedUsers.includes(name)) { 86 | node.style.color = theme.palette.success.light; 87 | } else { 88 | node.style.color = theme.palette.text.primary; 89 | } 90 | }); 91 | }, [matchedUsers]); 92 | 93 | useEffect(() => { 94 | scrollToTarget(matchedUsers[matchedUserIndex], usersRefs); 95 | }, [matchedUserIndex]); 96 | 97 | return ( 98 | 112 | {users.length != 0 ? ( 113 | <> 114 | 128 | 138 | Search By Name/Phone 139 | { 141 | const target = event.target.value.trim().toLowerCase(); 142 | let matched = []; 143 | usersRefs.current.forEach((node, name) => { 144 | let phone = node.getAttribute("phone"); 145 | let _name = name.toLowerCase(); 146 | if ( 147 | (_name.includes(target) || phone.includes(target)) && 148 | target !== "" 149 | ) { 150 | matched.push(name); 151 | } 152 | }); 153 | setMatchedUsers(matched); 154 | }} 155 | type="text" 156 | endAdornment={ 157 | 158 | { 160 | if ( 161 | 0 <= matchedUserIndex && 162 | matchedUserIndex <= matchedUsers.length - 1 163 | ) { 164 | if (matchedUserIndex + 1 >= matchedUsers.length) { 165 | setMatchedUserIndex(0); 166 | } else { 167 | setMatchedUserIndex(matchedUserIndex + 1); 168 | } 169 | } else { 170 | setMatchedUserIndex(0); 171 | } 172 | }} 173 | edge="end" 174 | > 175 | {matchedUsers.length == 0 ? ( 176 | 177 | ) : ( 178 | 179 | )} 180 | 181 | 182 | } 183 | label="Search By Name/Phone" 184 | /> 185 | 186 | 195 | 196 | {category != undefined && getUsers(category).length != 0 197 | ? `User Found: ${getUsers(category).length}` 198 | : "Type"} 199 | 200 | 215 | 216 | 217 | 226 | {category != "" ? ( 227 | 236 | {getUsers(category).length != 0 ? ( 237 | getUsers(category).map((user) => { 238 | return ( 239 | 240 | 248 | { 254 | usersRefs.current.set(user.user_name, element); 255 | }} 256 | /> 257 | 262 | {!isSmallScreen ? ( 263 | <> 264 | 271 | 272 | ) : ( 273 |
274 | )} 275 |
276 | 277 |
278 | ); 279 | }) 280 | ) : ( 281 | 282 | 283 | 284 | )} 285 |
286 | ) : ( 287 |
288 | )} 289 |
290 | 291 | ) : ( 292 | 293 | )} 294 |
295 | ); 296 | } 297 | -------------------------------------------------------------------------------- /frontend/app/error/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Box, ScopedCssBaseline, Typography } from "@mui/material"; 3 | import { useEffect } from "react"; 4 | import { alive } from "../auth"; 5 | import { useRouter } from "next/navigation"; 6 | 7 | export default function ServerError(){ 8 | const router = useRouter(); 9 | useEffect(() => { 10 | const checkServer = async () => { 11 | let response = await alive(); 12 | if (response === true) { 13 | router.replace("/"); 14 | } 15 | }; 16 | const backendChecker = setInterval(checkServer, 1000); 17 | return () => { 18 | clearInterval(backendChecker); 19 | }; 20 | }, []); 21 | return ( 22 | 23 | 35 | 42 | {"Critical Error"} 43 | 44 | 51 | {"Backend Server Is Not Running"} 52 | 53 | 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /frontend/app/info.js: -------------------------------------------------------------------------------- 1 | const title = "Fast Logger"; 2 | 3 | export { title }; 4 | -------------------------------------------------------------------------------- /frontend/app/layout.js: -------------------------------------------------------------------------------- 1 | import { title } from "@/app/info"; 2 | export const metadata = { 3 | title: title, 4 | }; 5 | 6 | export default function RootLayout({ children }) { 7 | return ( 8 | 9 | 10 | 16 | 17 | 18 | 19 | {children} 20 | 21 | ); 22 | } -------------------------------------------------------------------------------- /frontend/app/login/create/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, useContext } from "react"; 4 | import { Box, Button, TextField } from "@mui/material"; 5 | import { textRandom, toTitleCase, textSequential } from "@/app/api/textEffect"; 6 | import { sharedStatesContext } from "../sharedContext"; 7 | import { createAdmin, eligibleForAdmin } from "@/app/auth"; 8 | import { useRouter } from "next/navigation"; 9 | import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; 10 | import { 11 | isValidNumber, 12 | isValidPassword, 13 | isValidUserName, 14 | } from "@/app/api/validators"; 15 | import Collapse from "@mui/material/Collapse"; 16 | import CheckIcon from "@mui/icons-material/Check"; 17 | 18 | const description = "Detected no users in the system."; 19 | 20 | export default function CreateAdmin() { 21 | // Display Purpose And Validation 22 | const router = useRouter(); 23 | const { setTitleVar, setDescriptionVar } = useContext(sharedStatesContext); 24 | const [phoneVar, phoneVarSet] = useState(""); 25 | const [passVar, passVarSet] = useState(""); 26 | const [userVar, userVarSet] = useState(""); 27 | let title = "Create Admin"; 28 | 29 | let phone = "Phone Number"; 30 | let pass = "Password"; 31 | let user = "Name"; 32 | 33 | function defaultDescription(speed = 1) { 34 | textRandom(title, setTitleVar, 7 * speed); 35 | textRandom(description, setDescriptionVar, 2 * speed); 36 | textRandom(phone, phoneVarSet, 10 * speed); 37 | textRandom(pass, passVarSet, 15 * speed); 38 | textRandom(user, userVarSet, 20 * speed); 39 | } 40 | 41 | const checkUsers = async () => { 42 | let response = await eligibleForAdmin(); 43 | if (response === "error") { 44 | router.replace("/error"); 45 | } else if (response.status != true) { 46 | router.replace("/login"); 47 | } else { 48 | defaultDescription(); 49 | } 50 | }; 51 | useEffect(() => { 52 | checkUsers(); 53 | }, []); 54 | // Display Purpose And Validation 55 | 56 | const [phoneError, phoneErrorSet] = useState(false); 57 | const [phoneErrorMsg, phoneErrorMsgSet] = useState(false); 58 | const [passError, passErrorSet] = useState(false); 59 | const [passErrorMsg, passErrorMsgSet] = useState(false); 60 | const [nameError, nameErrorSet] = useState(false); 61 | const [nameErrorMsg, nameErrorMsgSet] = useState(false); 62 | 63 | const [phoneValue, phoneValueSet] = useState(""); 64 | const [passValue, passValueSet] = useState(""); 65 | const [nameValue, nameValueSet] = useState(""); 66 | 67 | const [busy, busySet] = useState(false); 68 | const [clearedDisplay, clearedDisplaySet] = useState(true); 69 | 70 | function resetDisplay() { 71 | if (!clearedDisplay) { 72 | textRandom("Editing", setTitleVar, 1); 73 | setDescriptionVar("📝"); 74 | } 75 | clearedDisplaySet(true); 76 | } 77 | 78 | function handelPhone(value) { 79 | resetDisplay(); 80 | phoneErrorSet(false); 81 | phoneErrorMsgSet(""); 82 | let result = isValidNumber(value.target.value); 83 | if (!(typeof result === "boolean")) { 84 | phoneErrorSet(true); 85 | phoneErrorMsgSet(result); 86 | value.target.value = value.target.value.trim().replace(" ", ""); 87 | } else { 88 | let valueTemp = value.target.value.trim().replace(" ", ""); 89 | if (valueTemp[0] === "0") { 90 | value.target.value = "0" + parseInt(valueTemp); 91 | } else { 92 | value.target.value = parseInt(valueTemp); 93 | } 94 | phoneValueSet(value.target.value); 95 | } 96 | } 97 | 98 | function handelPassword(value) { 99 | resetDisplay(); 100 | passErrorSet(false); 101 | passErrorMsgSet(""); 102 | let result = isValidPassword(value.target.value); 103 | if (!(typeof result === "boolean")) { 104 | passErrorSet(true); 105 | passErrorMsgSet(result); 106 | } else { 107 | passValueSet(value.target.value); 108 | } 109 | } 110 | 111 | function handelName(value) { 112 | resetDisplay(); 113 | nameErrorSet(false); 114 | nameErrorMsgSet(""); 115 | let result = isValidUserName(value.target.value); 116 | if (!(typeof result === "boolean")) { 117 | nameErrorSet(true); 118 | nameErrorMsgSet(result); 119 | if (value.target.value[value.target.value.length - 1] === " ") { 120 | value.target.value = value.target.value.trim() + " "; 121 | } else { 122 | value.target.value = value.target.value.trim(); 123 | } 124 | 125 | value.target.value = toTitleCase(value.target.value); 126 | } else { 127 | value.target.value = toTitleCase(value.target.value); 128 | if (value.target.value[value.target.value.length - 1] === " ") { 129 | value.target.value = value.target.value.trim() + " "; 130 | } else { 131 | value.target.value = value.target.value.trim(); 132 | } 133 | nameValueSet(value.target.value.trim()); 134 | } 135 | } 136 | 137 | async function createAdminButton() { 138 | if (clearedDisplay) { 139 | textSequential("Confirm", setTitleVar, 1); 140 | let description = `Name: ${nameValue}\nPhone: ${phoneValue}\nPassword: ${passValue}`; 141 | 142 | textSequential(description, setDescriptionVar, 1); 143 | clearedDisplaySet(false); 144 | } else { 145 | busySet(true); 146 | let result = await createAdmin(nameValue, phoneValue, passValue); 147 | if (result) { 148 | router.replace("/login"); 149 | } 150 | } 151 | } 152 | 153 | return ( 154 | <> 155 | 158 | 166 | 174 | 183 | 191 | 192 | 1 && 195 | phoneValue.length > 1 && 196 | passValue.length > 1 && 197 | !phoneError && 198 | !passError && 199 | !nameError && 200 | !busy 201 | } 202 | > 203 | 206 | 207 | 208 | 209 | 210 | 211 | ); 212 | }; 213 | -------------------------------------------------------------------------------- /frontend/app/login/layout.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import styles from "./local.module.css"; 5 | import { sharedStatesContext } from "./sharedContext"; 6 | import Box from "@mui/material/Box"; 7 | import Typography from "@mui/material/Typography"; 8 | import useMediaQuery from "@mui/material/useMediaQuery"; 9 | import { useTheme } from "@mui/system"; 10 | 11 | export default function AuthLayout({ children }) { 12 | const [titleVar, setTitleVar] = useState(""); 13 | const [descriptionVar, setDescriptionVar] = useState(""); 14 | const theme = useTheme(); 15 | const isLGScreen = useMediaQuery(theme.breakpoints.up("lg")); 16 | return ( 17 | <> 18 | 28 | 45 | {!isLGScreen ? null : ( 46 | 59 | 64 | {titleVar} 65 | 66 |
    73 | {descriptionVar.split("\n").map((line, index) => ( 74 |
  • 75 | 83 | {line} 84 | 85 |
  • 86 | ))} 87 |
88 |
89 | )} 90 | 102 | 114 | {isLGScreen ? null : ( 115 | 124 | {titleVar} 125 | 126 | )} 127 | 130 | {children} 131 | 132 | 133 | 134 |
135 |
136 | 137 | ); 138 | } -------------------------------------------------------------------------------- /frontend/app/login/local.module.css: -------------------------------------------------------------------------------- 1 | .shadow { 2 | box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.558); 3 | } 4 | 5 | .duel_tone { 6 | background: url('data:image/svg+xml;utf8,') 7 | no-repeat center center fixed; 8 | background-size: cover; 9 | } 10 | 11 | .whiteBG { 12 | background: white; 13 | width: 100vw; 14 | height: 100vh; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/app/login/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | import { useState, useEffect, useContext, useRef } from "react"; 4 | 5 | import { sharedStatesContext } from "./sharedContext"; 6 | import { textRandom, textSequential } from "@/app/api/textEffect"; 7 | import { 8 | getAllUsers, 9 | authorizeUser, 10 | isSessionValid, 11 | eligibleForAdmin, 12 | } from "@/app/auth"; 13 | import { isValidNumber, isValidPassword } from "@/app/api/validators"; 14 | 15 | import Button from "@mui/material/Button"; 16 | import IconButton from "@mui/material/IconButton"; 17 | import OutlinedInput from "@mui/material/OutlinedInput"; 18 | import InputLabel from "@mui/material/InputLabel"; 19 | import InputAdornment from "@mui/material/InputAdornment"; 20 | import FormHelperText from "@mui/material/FormHelperText"; 21 | import FormControl from "@mui/material/FormControl"; 22 | import TextField from "@mui/material/TextField"; 23 | import Alert from "@mui/material/Alert"; 24 | import Collapse from "@mui/material/Collapse"; 25 | 26 | import Visibility from "@mui/icons-material/Visibility"; 27 | import VisibilityOff from "@mui/icons-material/VisibilityOff"; 28 | import VpnKeyIcon from "@mui/icons-material/VpnKey"; 29 | import { title as globalTitle } from "@/app/info"; 30 | import { Box } from "@mui/material"; 31 | 32 | const description = 33 | "Supports parallel engines.\nAuto deletes old logs.\nFully responsive UI.\nCustom engine (Written In Python3).\nOptimized for multi-core processors."; 34 | 35 | export default function LogIn() { 36 | // Display Purpose And Validation 37 | const router = useRouter(); 38 | const { setTitleVar, setDescriptionVar } = useContext(sharedStatesContext); 39 | const [phoneVar, phoneVarSet] = useState(""); 40 | const [passVar, passVarSet] = useState(""); 41 | let title = globalTitle; 42 | 43 | let phone = "Phone Number"; 44 | let pass = "Password"; 45 | useEffect(() => { 46 | const checkUsers = async () => { 47 | let response = await eligibleForAdmin(); 48 | if (response === "error") { 49 | router.replace("/error"); 50 | } else if (response.status == true) { 51 | router.replace("/login/create"); 52 | } else if (isSessionValid()) { 53 | router.replace("/"); 54 | } else { 55 | textRandom(title, setTitleVar, 10); 56 | textSequential(description, setDescriptionVar, 1); 57 | textRandom(phone, phoneVarSet, 10); 58 | textRandom(pass, passVarSet, 15); 59 | } 60 | }; 61 | checkUsers(); 62 | }, []); 63 | // Display Purpose And Validation 64 | 65 | const [phoneValue, phoneValueSet] = useState(""); 66 | const [passValue, passValueSet] = useState(""); 67 | 68 | const [phoneError, phoneErrorSet] = useState(false); 69 | const [passError, passErrorSet] = useState(false); 70 | const [phoneErrorMsg, phoneErrorMsgSet] = useState(false); 71 | const [passErrorMsg, passErrorMsgSet] = useState(false); 72 | const [showPassword, setShowPassword] = useState(false); 73 | const [alert, setAlert] = useState(false); 74 | 75 | function togglePassword() { 76 | setShowPassword((show) => !show); 77 | } 78 | 79 | function handelPhone(value) { 80 | phoneErrorSet(false); 81 | phoneErrorMsgSet(""); 82 | let result = isValidNumber(value.target.value); 83 | if (!(typeof result === "boolean")) { 84 | phoneErrorSet(true); 85 | phoneErrorMsgSet(result); 86 | value.target.value = value.target.value.trim().replace(" ", ""); 87 | } else { 88 | let valueTemp = value.target.value.trim().replace(" ", ""); 89 | if (valueTemp[0] === "0") { 90 | value.target.value = "0" + parseInt(valueTemp); 91 | } else { 92 | value.target.value = parseInt(valueTemp); 93 | } 94 | phoneValueSet(value.target.value); 95 | } 96 | } 97 | 98 | function handelPassword(value) { 99 | passErrorSet(false); 100 | passErrorMsgSet(""); 101 | let result = isValidPassword(value.target.value); 102 | if (!(typeof result === "boolean")) { 103 | passErrorSet(true); 104 | passErrorMsgSet(result); 105 | } else { 106 | passValueSet(value.target.value); 107 | } 108 | } 109 | 110 | async function logIn() { 111 | if (await authorizeUser(phoneValue, passValue)) { 112 | router.replace("/"); 113 | } else { 114 | setAlert(true); 115 | phoneErrorSet(true); 116 | passErrorSet(true); 117 | } 118 | } 119 | 120 | return ( 121 | <> 122 | 125 | 126 | { 129 | setAlert(false); 130 | phoneErrorSet(false); 131 | passErrorSet(false); 132 | phoneErrorMsgSet(""); 133 | passErrorMsgSet(""); 134 | }} 135 | > 136 | Invalid Credentials! 137 | 138 | 139 | 146 | 151 | 152 | {passVar} 153 | 154 | 160 | 165 | {showPassword ? : } 166 | 167 | 168 | } 169 | label={passVar} 170 | /> 171 | 172 | {passErrorMsg} 173 | 174 | 175 | 183 | 184 | 185 | 188 | 189 | 190 | 191 | 192 | 193 | ); 194 | } 195 | -------------------------------------------------------------------------------- /frontend/app/login/sharedContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export const sharedStatesContext = createContext(null); -------------------------------------------------------------------------------- /frontend/app/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | import React, { useEffect } from "react"; 4 | import ScopedCssBaseline from "@mui/material/ScopedCssBaseline"; 5 | import CircularProgress from "@mui/material/CircularProgress"; 6 | import { isSessionValid, alive } from "@/app/auth"; 7 | import { Box } from "@mui/material"; 8 | 9 | export default function Root() { 10 | const router = useRouter(); 11 | useEffect(() => { 12 | const checkServer = async () => { 13 | let response = await alive(); 14 | if (response != true) { 15 | router.replace("/error"); 16 | } 17 | }; 18 | // for immediate redirect 19 | if (!isSessionValid()) { 20 | router.replace("/login"); 21 | } else { 22 | router.replace("/dashboard/system"); 23 | } 24 | checkServer(); 25 | }, []); 26 | return ( 27 | <> 28 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -H 127.0.0.1 -p 2200", 7 | "build": "next build", 8 | "start": "next start -H 127.0.0.1 -p 3200", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "11.11.3", 13 | "@emotion/styled": "11.11.0", 14 | "@mui/icons-material": "5.15.2", 15 | "@mui/lab": "5.0.0-alpha.158", 16 | "@mui/material": "5.15.2", 17 | "@mui/x-charts": "6.18.4", 18 | "@mui/x-date-pickers": "6.18.6", 19 | "animejs": "3.2.2", 20 | "axios": "1.6.2", 21 | "luxon": "3.4.4", 22 | "material-react-table": "2.1.0", 23 | "next": "14.0.4", 24 | "react": "18", 25 | "react-dom": "18" 26 | }, 27 | "devDependencies": { 28 | "eslint": "8", 29 | "eslint-config-next": "14.0.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 21 | 24 | 28 | 36 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if running with sudo 4 | if [ "$(id -u)" -ne 0 ]; then 5 | echo "install script must be run with sudo or as root. exiting." 6 | exit 1 7 | fi 8 | 9 | 10 | destination_path="/home/$SUDO_USER/fast_logger" 11 | node_v="20" 12 | 13 | sudo -u $SUDO_USER mkdir -p "$destination_path" 14 | 15 | sudo -u $SUDO_USER cp -r "./backend" "$destination_path" 16 | sudo -u $SUDO_USER cp -r "./frontend" "$destination_path" 17 | sudo -u $SUDO_USER cp -r "GLOBAL_CONFIG.JSON" "$destination_path" 18 | 19 | echo '{ 20 | "FAST_API_SERVER_IP_WILL_AUTO_GENERATE": "WILL_AUTO_GENERATE", 21 | "FAST_API_AUTH_SALT_WILL_AUTO_GENERATE": "WILL_AUTO_GENERATE" 22 | }' > "$destination_path"/AUTO_CONFIG.JSON 23 | chown "$SUDO_USER:$SUDO_USER" "$destination_path/AUTO_CONFIG.JSON" 24 | # dependencies and venv 25 | 26 | packages=( 27 | python3-dev 28 | python3-venv 29 | build-essential 30 | ) 31 | 32 | sudo apt update 33 | sudo apt install -y "${packages[@]}" 34 | 35 | #python/backend 36 | python3 -m venv "$destination_path/backend/.venv" 37 | source "$destination_path/backend/.venv/bin/activate" 38 | pip install -r "$destination_path/backend/requirements.txt" 39 | deactivate 40 | #python/backend 41 | 42 | #node/frontend 43 | sudo apt-get install -y ca-certificates curl gnupg 44 | sudo mkdir -p /etc/apt/keyrings 45 | sudo rm /etc/apt/keyrings/nodesource.gpg 46 | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg 47 | echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$node_v.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list 48 | sudo apt update 49 | sudo apt install -y nodejs 50 | npm --prefix "$destination_path/frontend" install 51 | npm --prefix "$destination_path/frontend" run build 52 | #node/frontend 53 | 54 | # dependencies and venv 55 | 56 | #run script 57 | echo -e "#!/bin/bash\nsource \"$destination_path/backend/.venv/bin/activate\"\ncd \"\$(dirname \"\${BASH_SOURCE[0]}\")/backend\" || exit 1\npython3 ./server.py &\nsleep 10\ncd ../frontend || exit 1\nnpm run start\ndeactivate" > "$destination_path/start.sh" 58 | chmod +x "$destination_path/start.sh" 59 | #run script 60 | 61 | # run on boot and allow timedatectl free from passwords 62 | USERNAME=$(logname) 63 | 64 | echo "$USERNAME ALL=(ALL:ALL) NOPASSWD: /usr/bin/timedatectl set-timezone *" | sudo bash -c "cat >> /etc/sudoers" 65 | echo "$USERNAME ALL=(ALL:ALL) NOPASSWD: /usr/bin/timedatectl set-ntp true" | sudo bash -c "cat >> /etc/sudoers" 66 | echo "$USERNAME ALL=(ALL:ALL) NOPASSWD: /sbin/reboot -f" | sudo bash -c "cat >> /etc/sudoers" 67 | 68 | cat < "/etc/systemd/system/fast_logger_start.service" 69 | [Unit] 70 | Description=Fast Logger Start Script 71 | After=multi-user.target 72 | 73 | [Service] 74 | Type=simple 75 | ExecStart="$destination_path/start.sh" 76 | 77 | [Install] 78 | WantedBy=multi-user.target 79 | EOL 80 | 81 | chmod 644 "/etc/systemd/system/fast_logger_start.service" 82 | 83 | systemctl daemon-reload 84 | systemctl enable fast_logger_start.service 85 | 86 | # run on boot and allow timedatectl free from passwords 87 | 88 | # production mode 89 | echo '{"DEVELOPMENT": false}' > "$destination_path/MODE.JSON" 90 | # production mode 91 | 92 | ## nginx 93 | sudo apt-get install -y nginx 94 | 95 | sudo mkdir -p /etc/nginx/old-sites-configs/sites-available/ 96 | sudo mkdir -p /etc/nginx/old-sites-configs/sites-enabled/ 97 | 98 | sudo mv /etc/nginx/sites-available/* /etc/nginx/old-sites-configs/sites-available/ 99 | sudo mv /etc/nginx/sites-enabled/* /etc/nginx/old-sites-configs/sites-enabled/ 100 | 101 | conf_path="/etc/nginx/sites-available/fastlogger.conf" 102 | 103 | ssl_certificates_path="$destination_path/ssl" 104 | 105 | if [ ! -d "$ssl_certificates_path" ]; then 106 | sudo -u "$USER" mkdir -p "$ssl_certificates_path" 107 | fi 108 | 109 | fastlogger_config=" 110 | server { 111 | listen 80 default_server; 112 | listen [::]:80 default_server; 113 | server_name _; 114 | return 301 https://\$host\$request_uri; 115 | } 116 | 117 | server { 118 | listen 443 ssl default_server; 119 | listen [::]:443 ssl default_server; 120 | server_name _; 121 | ssl_certificate $ssl_certificates_path/nginx.crt; 122 | ssl_certificate_key $ssl_certificates_path/nginx.key; 123 | 124 | location / { 125 | proxy_pass http://127.0.0.1:3200; 126 | proxy_http_version 1.1; 127 | proxy_set_header Origin http://127.0.0.1:3200; 128 | } 129 | 130 | location /download { 131 | proxy_pass http://127.0.0.1:3000/download; 132 | proxy_http_version 1.1; 133 | } 134 | 135 | location /realtime { 136 | proxy_pass http://127.0.0.1:3000/realtime; 137 | proxy_http_version 1.1; 138 | proxy_set_header Upgrade \$http_upgrade; 139 | proxy_set_header Connection \"upgrade\"; 140 | proxy_set_header Host \$host; 141 | proxy_set_header X-Real-IP \$remote_addr; 142 | proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; 143 | proxy_set_header X-Forwarded-Proto \$scheme; 144 | } 145 | } 146 | " 147 | 148 | echo -e "$fastlogger_config" | sudo dd of="$conf_path" 149 | sudo ln -s "$conf_path" "/etc/nginx/sites-enabled/" 150 | #self signed cert 151 | sudo openssl genpkey -algorithm RSA -out $ssl_certificates_path/nginx.key > /dev/null 2>&1 152 | sudo openssl req -x509 -key $ssl_certificates_path/nginx.key -out $ssl_certificates_path/nginx.crt -days 365 -subj "/CN=fastlogger.app" > /dev/null 2>&1 153 | #self signed cert 154 | sudo systemctl restart nginx 155 | ## ngnix 156 | 157 | 158 | 159 | 160 | 161 | echo "installed at $destination_path" 162 | read -p "press enter to exit, system will reboot" 163 | sudo reboot -f 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if running with sudo 4 | if [ "$(id -u)" -ne 0 ]; then 5 | echo "uninstall script must be run with sudo or as root. exiting." 6 | exit 1 7 | fi 8 | 9 | destination_path="/home/$SUDO_USER/fast_logger" 10 | 11 | rm -rf "/home/$SUDO_USER/fast_logger" 12 | rm -f "/etc/systemd/system/fast_logger_start.service" 13 | 14 | sudo sed -i '/timedatectl set-timezone/d' /etc/sudoers 15 | sudo sed -i '/timedatectl set-ntp true/d' /etc/sudoers 16 | sudo sed -i '/\/sbin\/reboot -f/d' /etc/sudoers 17 | sudo mv /etc/nginx/old-sites-configs/sites-available/* /etc/nginx/sites-available 18 | sudo mv /etc/nginx/old-sites-configs/sites-enabled/* /etc/nginx/sites-enabled 19 | sudo rm /etc/nginx/sites-enabled/fastlogger.conf 20 | sudo rm /etc/nginx/sites-available/fastlogger.conf 21 | sudo rm -r /etc/nginx/old-sites-configs 22 | sudo systemctl restart nginx 23 | read -p "press enter to exit, system will reboot" 24 | sudo reboot -f 25 | --------------------------------------------------------------------------------