Merge branch 'dev' into refactor/remove-api-v1
@ -3,9 +3,13 @@
|
||||
### New features
|
||||
TBD
|
||||
### Bug fixes
|
||||
TBD
|
||||
- Remove webhook `custom` option from webook provider list as it's not currently an option ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/664))
|
||||
### Tweaks
|
||||
TBD
|
||||
- Homogenize Panel logos/branding ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/666))
|
||||
- Retain previous tab when revisiting server details page (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667))
|
||||
- Add server name tag in panel header (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667))
|
||||
- Setup logging for panel authentication attempts ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669))
|
||||
- Update minimum password length from 6 to 8, and unrestrict maximum password length ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669))
|
||||
### Lang
|
||||
TBD
|
||||
<br><br>
|
||||
|
@ -36,6 +36,7 @@ class ServersController(metaclass=Singleton):
|
||||
self.management_helper = management_helper
|
||||
self.servers_list = []
|
||||
self.stats = Stats(self.helper, self)
|
||||
self.server_subpage = {}
|
||||
|
||||
# **********************************************************************************
|
||||
# Generic Servers Methods
|
||||
|
@ -45,8 +45,7 @@ class UsersController:
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"maxLength": 20,
|
||||
"minLength": 6,
|
||||
"minLength": 8,
|
||||
"examples": ["crafty"],
|
||||
"title": "Password",
|
||||
},
|
||||
|
@ -78,6 +78,37 @@ class Controller:
|
||||
self.first_login = False
|
||||
self.cached_login = self.management.get_login_image()
|
||||
self.support_scheduler.start()
|
||||
try:
|
||||
with open(
|
||||
os.path.join(os.path.curdir, "logs", "auth_tracker.log"),
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
self.auth_tracker = json.load(f)
|
||||
except:
|
||||
self.auth_tracker = {}
|
||||
|
||||
def log_attempt(self, remote_ip, username):
|
||||
remote = self.auth_tracker.get(str(remote_ip), None)
|
||||
if remote:
|
||||
remote["names"].append(username)
|
||||
remote["attempts"] += 1
|
||||
remote["times"].append(datetime.now().strftime("%d/%m/%Y %H:%M:%S"))
|
||||
self.auth_tracker[str(remote_ip)] = remote
|
||||
else:
|
||||
self.auth_tracker[str(remote_ip)] = {
|
||||
"names": [username],
|
||||
"attempts": 1,
|
||||
"times": [datetime.now().strftime("%d/%m/%Y %H:%M:%S")],
|
||||
}
|
||||
|
||||
def write_auth_tracker(self):
|
||||
with open(
|
||||
os.path.join(os.path.curdir, "logs", "auth_tracker.log"),
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
json.dump(self.auth_tracker, f, indent=4)
|
||||
|
||||
@staticmethod
|
||||
def check_system_user():
|
||||
|
@ -201,6 +201,13 @@ class TasksManager:
|
||||
id="update_watcher",
|
||||
start_date=datetime.datetime.now(),
|
||||
)
|
||||
self.scheduler.add_job(
|
||||
self.controller.write_auth_tracker,
|
||||
"interval",
|
||||
minutes=5,
|
||||
id="auth_tracker_write",
|
||||
start_date=datetime.datetime.now(),
|
||||
)
|
||||
# self.scheduler.add_job(
|
||||
# self.scheduler.print_jobs, "interval", seconds=10, id="-1"
|
||||
# )
|
||||
|
@ -14,6 +14,7 @@ from app.classes.shared.translation import Translation
|
||||
from app.classes.shared.main_models import DatabaseShortcuts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
auth_log = logging.getLogger("auth")
|
||||
|
||||
bearer_pattern = re.compile(r"^Bearer ", flags=re.IGNORECASE)
|
||||
|
||||
@ -231,9 +232,16 @@ class BaseHandler(tornado.web.RequestHandler):
|
||||
user,
|
||||
)
|
||||
logging.debug("Auth unsuccessful")
|
||||
auth_log.error(
|
||||
f"Authentication attempted from {self.get_remote_ip()}. Invalid token"
|
||||
)
|
||||
self.access_denied(None, "the user provided an invalid token")
|
||||
return None
|
||||
except Exception as auth_exception:
|
||||
auth_log.error(
|
||||
f"Authentication attempted from {self.get_remote_ip()}."
|
||||
f" Error: {auth_exception}"
|
||||
)
|
||||
logger.debug(
|
||||
"An error occured while authenticating an API user:",
|
||||
exc_info=auth_exception,
|
||||
|
@ -481,6 +481,12 @@ class PanelHandler(BaseHandler):
|
||||
subpage = nh3.clean(self.get_argument("subpage", ""))
|
||||
|
||||
server_id = self.check_server_id()
|
||||
# load page the user was on last
|
||||
server_subpage = self.controller.servers.server_subpage.get(server_id, "")
|
||||
if subpage == "" and server_subpage != "":
|
||||
subpage = self.controller.servers.server_subpage.get(server_id, "")
|
||||
else:
|
||||
self.controller.servers.server_subpage[server_id] = subpage
|
||||
if server_id is None:
|
||||
return
|
||||
if not self.failed_server:
|
||||
|
@ -6,6 +6,7 @@ from app.classes.models.users import HelperUsers
|
||||
from app.classes.web.base_handler import BaseHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
auth_log = logging.getLogger("auth")
|
||||
|
||||
|
||||
class PublicHandler(BaseHandler):
|
||||
@ -96,6 +97,9 @@ class PublicHandler(BaseHandler):
|
||||
page_data["query"] = self.request.query
|
||||
|
||||
if page == "login":
|
||||
auth_log.info(
|
||||
f"User attempting to authenticate from {self.get_remote_ip()}"
|
||||
)
|
||||
next_page = "/login"
|
||||
if self.request.query:
|
||||
next_page = "/login?" + self.request.query
|
||||
@ -108,6 +112,12 @@ class PublicHandler(BaseHandler):
|
||||
user_id = HelperUsers.get_user_id_by_name(entered_username.lower())
|
||||
user_data = HelperUsers.get_user_model(user_id)
|
||||
except:
|
||||
self.controller.log_attempt(self.get_remote_ip(), entered_username)
|
||||
auth_log.error(
|
||||
f"User attempted to log into {entered_username}."
|
||||
f" Authentication failed from remote IP {self.get_remote_ip()}"
|
||||
" Users does not exist."
|
||||
)
|
||||
error_msg = "Incorrect username or password. Please try again."
|
||||
# self.clear_cookie("user")
|
||||
# self.clear_cookie("user_data")
|
||||
@ -120,6 +130,12 @@ class PublicHandler(BaseHandler):
|
||||
|
||||
# if we don't have a user
|
||||
if not user_data:
|
||||
auth_log.error(
|
||||
f"User attempted to log into {entered_username}. Authentication"
|
||||
f" failed from remote IP {self.get_remote_ip()}"
|
||||
" User does not exist."
|
||||
)
|
||||
self.controller.log_attempt(self.get_remote_ip(), entered_username)
|
||||
error_msg = "Incorrect username or password. Please try again."
|
||||
# self.clear_cookie("user")
|
||||
# self.clear_cookie("user_data")
|
||||
@ -132,6 +148,12 @@ class PublicHandler(BaseHandler):
|
||||
|
||||
# if they are disabled
|
||||
if not user_data.enabled:
|
||||
auth_log.error(
|
||||
f"User attempted to log into {entered_username}. "
|
||||
f"Authentication failed from remote IP {self.get_remote_ip()}."
|
||||
" User account disabled"
|
||||
)
|
||||
self.controller.log_attempt(self.get_remote_ip(), entered_username)
|
||||
error_msg = (
|
||||
"User account disabled. Please contact "
|
||||
"your system administrator for more info."
|
||||
@ -159,7 +181,11 @@ class PublicHandler(BaseHandler):
|
||||
user_data.last_ip = self.get_remote_ip()
|
||||
user_data.last_login = Helpers.get_time_as_string()
|
||||
user_data.save()
|
||||
|
||||
auth_log.info(
|
||||
f"{entered_username} successfully"
|
||||
" authenticated and logged"
|
||||
f" into panel from remote IP {self.get_remote_ip()}"
|
||||
)
|
||||
# log this login
|
||||
self.controller.management.add_to_audit_log(
|
||||
user_data.user_id, "Logged in", 0, self.get_remote_ip()
|
||||
@ -172,6 +198,11 @@ class PublicHandler(BaseHandler):
|
||||
|
||||
self.redirect(next_page)
|
||||
else:
|
||||
auth_log.error(
|
||||
f"User attempted to log into {entered_username}."
|
||||
f" Authentication failed from remote IP {self.get_remote_ip()}"
|
||||
)
|
||||
self.controller.log_attempt(self.get_remote_ip(), entered_username)
|
||||
# self.clear_cookie("user")
|
||||
# self.clear_cookie("user_data")
|
||||
self.clear_cookie("token")
|
||||
|
@ -7,7 +7,7 @@ from app.classes.shared.helpers import Helpers
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
auth_log = logging.getLogger("auth")
|
||||
login_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -29,6 +29,10 @@ class ApiAuthLoginHandler(BaseApiHandler):
|
||||
try:
|
||||
data = json.loads(self.request.body)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
logger.error(
|
||||
"Invalid JSON schema for API"
|
||||
f" login attempt from {self.get_remote_ip()}"
|
||||
)
|
||||
return self.finish_json(
|
||||
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
||||
)
|
||||
@ -36,6 +40,10 @@ class ApiAuthLoginHandler(BaseApiHandler):
|
||||
try:
|
||||
validate(data, login_schema)
|
||||
except ValidationError as e:
|
||||
logger.error(
|
||||
"Invalid JSON schema for API"
|
||||
f" login attempt from {self.get_remote_ip()}"
|
||||
)
|
||||
return self.finish_json(
|
||||
400,
|
||||
{
|
||||
@ -52,12 +60,23 @@ class ApiAuthLoginHandler(BaseApiHandler):
|
||||
user_data = Users.get_or_none(Users.username == username)
|
||||
|
||||
if user_data is None:
|
||||
self.controller.log_attempt(self.get_remote_ip(), username)
|
||||
auth_log.error(
|
||||
f"User attempted to log into {username}."
|
||||
" Authentication failed from remote IP"
|
||||
f" {self.get_remote_ip()}. User not found"
|
||||
)
|
||||
return self.finish_json(
|
||||
401,
|
||||
{"status": "error", "error": "INCORRECT_CREDENTIALS", "token": None},
|
||||
)
|
||||
|
||||
if not user_data.enabled:
|
||||
auth_log.error(
|
||||
f"User attempted to log into {username}."
|
||||
" Authentication failed from remote"
|
||||
f" IP {self.get_remote_ip()} account disabled"
|
||||
)
|
||||
self.finish_json(
|
||||
403, {"status": "error", "error": "ACCOUNT_DISABLED", "token": None}
|
||||
)
|
||||
@ -67,6 +86,11 @@ class ApiAuthLoginHandler(BaseApiHandler):
|
||||
|
||||
# Valid Login
|
||||
if login_result:
|
||||
auth_log.info(
|
||||
f"{username} successfully"
|
||||
" authenticated and logged"
|
||||
f" into panel from remote IP {self.get_remote_ip()}"
|
||||
)
|
||||
logger.info(f"User: {user_data} Logged in from IP: {self.get_remote_ip()}")
|
||||
|
||||
# record this login
|
||||
|
@ -10,16 +10,17 @@
|
||||
},
|
||||
"schedule": {
|
||||
"format": "%(asctime)s - [Schedules] - %(levelname)s - %(message)s"
|
||||
},
|
||||
"auth": {
|
||||
"format": "%(asctime)s - [AUTH] - %(levelname)s - %(message)s"
|
||||
}
|
||||
},
|
||||
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "commander",
|
||||
"stream": "ext://sys.stdout"
|
||||
},
|
||||
|
||||
"main_file_handler": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"formatter": "commander",
|
||||
@ -50,24 +51,45 @@
|
||||
"maxBytes": 10485760,
|
||||
"backupCount": 20,
|
||||
"encoding": "utf8"
|
||||
},
|
||||
"auth_file_handler": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"formatter": "auth",
|
||||
"filename": "logs/auth.log",
|
||||
"maxBytes": 10485760,
|
||||
"backupCount": 20,
|
||||
"encoding": "utf8"
|
||||
}
|
||||
},
|
||||
|
||||
"loggers": {
|
||||
"": {
|
||||
"level": "INFO",
|
||||
"handlers": ["main_file_handler", "session_file_handler"],
|
||||
"handlers": [
|
||||
"main_file_handler",
|
||||
"session_file_handler"
|
||||
],
|
||||
"propagate": false
|
||||
},
|
||||
"tornado.access": {
|
||||
"level": "INFO",
|
||||
"handlers": ["tornado_access_file_handler"],
|
||||
"handlers": [
|
||||
"tornado_access_file_handler"
|
||||
],
|
||||
"propagate": false
|
||||
},
|
||||
"apscheduler": {
|
||||
"level": "INFO",
|
||||
"handlers": ["schedule_file_handler"],
|
||||
"handlers": [
|
||||
"schedule_file_handler"
|
||||
],
|
||||
"propagate": false
|
||||
},
|
||||
"auth": {
|
||||
"level": "INFO",
|
||||
"handlers": [
|
||||
"auth_file_handler"
|
||||
],
|
||||
"propagate": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 424 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.6 KiB |
10
app/frontend/static/assets/images/crafty-logo-square.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 425 425">
|
||||
<!-- Background -->
|
||||
<rect x="0" y="0" rx="105" ry="105" width="425" height="425" style="fill: #22283A"/>
|
||||
<!-- green C -->
|
||||
<path fill="hsl(160, 59%, 45%)" d="M 162.5 182.5 l 38 -15 q 0 -40 -40 -40 l -95 0 q -40 0 -40 40 l 0 90 q 0 40 40 40 l 95 0 q 40 0 40 -40 l -38 -20 l 0 28 l -100 0 l 0 -108 l 100 0 z"/>
|
||||
<!-- blue C -->
|
||||
<path fill="hsl(192, 99%, 45%)" d="M 262.5 182.5 l -38 -15 q 0 -40 40 -40 l 95 0 q 40 0 40 40 l 0 90 q 0 40 -40 40 l -95 0 q -40 0 -40 -40 l 38 -20 l 0 28 l 100 0 l 0 -108 l -100 0 z"/>
|
||||
<!-- line thing in middle of Cs -->
|
||||
<path fill="hsl(266, 71%, 57%)" d="M 142.5 191.5 q -10 0 -10 10 l 0 19 q 0 10 10 10 l 140 0 q 10 0 10 -10 l 0 -19 q 0 -10 -10 -10 z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 750 B |
@ -82,6 +82,9 @@
|
||||
<span class="mdi mdi-chevron-double-left"></span>
|
||||
<span class="mdi mdi-chevron-double-right"></span>
|
||||
</button>
|
||||
|
||||
<span class="badge-pill badge-outline-primary" id="server-name-nav" style="display: none;"></span>
|
||||
|
||||
|
||||
{% include notify.html %}
|
||||
|
||||
|
@ -248,12 +248,38 @@
|
||||
$("#player-body").html(text);
|
||||
|
||||
}
|
||||
|
||||
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
|
||||
function getCookie(name) {
|
||||
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
|
||||
return r ? r[1] : undefined;
|
||||
}
|
||||
const token = getCookie("_xsrf")
|
||||
$(window).ready(function () {
|
||||
console.log("ready!");
|
||||
|
||||
//if (webSocket) {
|
||||
webSocket.on('update_server_details', update_server_details);
|
||||
add_server_name();
|
||||
//}
|
||||
});
|
||||
async function add_server_name(){
|
||||
let res = await fetch(`/api/v2/servers/${serverId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-XSRFToken': token
|
||||
},
|
||||
});
|
||||
let responseData = await res.json();
|
||||
if (responseData.status === "ok") {
|
||||
console.log(responseData)
|
||||
$("#server-name-nav").html(`${responseData.data['server_name']}`)
|
||||
$("#server-name-nav").show();
|
||||
} else {
|
||||
|
||||
bootbox.alert({
|
||||
title: responseData.error,
|
||||
message: responseData.error_data
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
@ -43,8 +43,10 @@
|
||||
<form class="forms-sample" method="post" id="webhook_form"
|
||||
action="/panel/edit_webhook?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{ data['webhook']['id'] }}">
|
||||
{% end %}
|
||||
<select class="form-select form-control form-control-lg select-css" id="webhook_type" name="webhook_type">
|
||||
<select class="form-select form-control form-control-lg select-css" id="webhook_type" name="webhook_type">
|
||||
{% if data['new_webhook'] == False %}
|
||||
<option value="{{data['webhook']['webhook_type']}}">{{data['webhook']['webhook_type']}}</option>
|
||||
{% end %}
|
||||
{% for type in data['providers'] %}
|
||||
{% if type != data['webhook']['webhook_type'] %}
|
||||
<option value="{{type}}">{{type}}</option>
|
||||
|
8
main.py
@ -73,6 +73,14 @@ def do_intro():
|
||||
|
||||
def setup_logging(debug=True):
|
||||
logging_config_file = os.path.join(os.path.curdir, "app", "config", "logging.json")
|
||||
if not helper.check_file_exists(
|
||||
os.path.join(os.path.curdir, "logs", "auth_tracker.log")
|
||||
):
|
||||
open(
|
||||
os.path.join(os.path.curdir, "logs", "auth_tracker.log"),
|
||||
"a",
|
||||
encoding="utf-8",
|
||||
).close()
|
||||
|
||||
if os.path.exists(logging_config_file):
|
||||
# open our logging config file
|
||||
|