Update public source
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
cracklab_bp = Blueprint(
|
||||
"cracklab",
|
||||
__name__,
|
||||
url_prefix="/cracklab",
|
||||
template_folder="templates",
|
||||
static_folder="static",
|
||||
static_url_path="/static",
|
||||
)
|
||||
|
||||
|
||||
from cracklab.server.web import routes # noqa: E402,F401
|
||||
@@ -0,0 +1,408 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections import Counter
|
||||
|
||||
from flask import redirect, render_template, request, url_for
|
||||
|
||||
from cracklab.server.db.connection import connect_database
|
||||
from cracklab.server.repositories.clients import ClientsRepository
|
||||
from cracklab.server.repositories.jobs import JobsRepository
|
||||
from cracklab.server.repositories.methods import MethodsRepository
|
||||
from cracklab.server.repositories.results import ResultsRepository
|
||||
from cracklab.server.services.methods import MethodService, MethodServiceError
|
||||
|
||||
from cracklab.server.web import cracklab_bp
|
||||
|
||||
|
||||
@cracklab_bp.route("/")
|
||||
def dashboard():
|
||||
conn = connect_database()
|
||||
|
||||
try:
|
||||
methods = MethodsRepository(conn).list_methods()
|
||||
jobs = JobsRepository(conn).list_jobs()
|
||||
clients = ClientsRepository(conn).list_clients()
|
||||
results = ResultsRepository(conn).list_results()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
job_statuses = Counter(
|
||||
row["status"]
|
||||
for row in jobs
|
||||
)
|
||||
|
||||
recent_jobs = jobs[:10]
|
||||
|
||||
method_names = {
|
||||
row["id"]: row["name"]
|
||||
for row in methods
|
||||
}
|
||||
|
||||
client_names = {
|
||||
row["id"]: row["name"]
|
||||
for row in clients
|
||||
}
|
||||
|
||||
return render_template(
|
||||
"dashboard.html",
|
||||
active_page="dashboard",
|
||||
methods_count=len(methods),
|
||||
jobs_count=len(jobs),
|
||||
clients_count=len(clients),
|
||||
results_count=len(results),
|
||||
active_jobs_count=sum(
|
||||
job_statuses.get(status, 0)
|
||||
for status in (
|
||||
"CREATED",
|
||||
"QUEUED",
|
||||
"RUNNING",
|
||||
"PARTIAL",
|
||||
)
|
||||
),
|
||||
job_statuses=job_statuses,
|
||||
recent_jobs=recent_jobs,
|
||||
method_names=method_names,
|
||||
client_names=client_names,
|
||||
)
|
||||
|
||||
|
||||
@cracklab_bp.route("/jobs/")
|
||||
def jobs_page():
|
||||
return _placeholder(
|
||||
"Jobs",
|
||||
"Job management will be implemented here.",
|
||||
"jobs",
|
||||
)
|
||||
|
||||
|
||||
@cracklab_bp.route("/methods/")
|
||||
def methods_page():
|
||||
conn = connect_database()
|
||||
|
||||
try:
|
||||
service = MethodService(conn)
|
||||
methods = service.list_methods()
|
||||
|
||||
method_rows = []
|
||||
|
||||
for method in methods:
|
||||
versions = service.list_method_versions(method["id"])
|
||||
|
||||
latest = versions[-1] if versions else None
|
||||
|
||||
steps_count = 0
|
||||
|
||||
if latest is not None:
|
||||
try:
|
||||
definition = json.loads(
|
||||
latest["definition_json"]
|
||||
)
|
||||
steps_count = len(
|
||||
definition.get("steps", [])
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
steps_count = 0
|
||||
|
||||
method_rows.append(
|
||||
{
|
||||
"id": method["id"],
|
||||
"name": method["name"],
|
||||
"description": method["description"],
|
||||
"created_at": method["created_at"],
|
||||
"version": (
|
||||
latest["version"]
|
||||
if latest is not None
|
||||
else None
|
||||
),
|
||||
"steps_count": steps_count,
|
||||
}
|
||||
)
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return render_template(
|
||||
"methods.html",
|
||||
active_page="methods",
|
||||
methods=method_rows,
|
||||
)
|
||||
|
||||
|
||||
@cracklab_bp.route("/methods/create/", methods=["GET", "POST"])
|
||||
def method_create_page():
|
||||
error = None
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
name = request.form.get("name", "").strip()
|
||||
description = request.form.get(
|
||||
"description",
|
||||
"",
|
||||
).strip()
|
||||
|
||||
hash_mode_raw = request.form.get(
|
||||
"hash_mode",
|
||||
"22000",
|
||||
).strip()
|
||||
|
||||
if not name:
|
||||
raise MethodServiceError(
|
||||
"Введите название метода."
|
||||
)
|
||||
|
||||
if hash_mode_raw != "22000":
|
||||
raise MethodServiceError(
|
||||
"Сейчас поддерживается только hash mode 22000."
|
||||
)
|
||||
|
||||
hash_mode = 22000
|
||||
|
||||
steps = _build_steps_from_form()
|
||||
|
||||
definition = {
|
||||
"schema_version": 1,
|
||||
"hash_mode": hash_mode,
|
||||
"steps": steps,
|
||||
}
|
||||
|
||||
conn = connect_database()
|
||||
|
||||
try:
|
||||
service = MethodService(conn)
|
||||
|
||||
with conn:
|
||||
method_id = service.create_method(
|
||||
name=name,
|
||||
description=description or None,
|
||||
)
|
||||
|
||||
service.create_method_version(
|
||||
method_id=method_id,
|
||||
version=1,
|
||||
definition=definition,
|
||||
)
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"cracklab.methods_page",
|
||||
)
|
||||
)
|
||||
|
||||
except (MethodServiceError, ValueError) as exc:
|
||||
error = str(exc)
|
||||
|
||||
return render_template(
|
||||
"method_create.html",
|
||||
active_page="methods",
|
||||
error=error,
|
||||
)
|
||||
|
||||
|
||||
def _build_steps_from_form() -> list[dict]:
|
||||
step_count_raw = request.form.get(
|
||||
"step_count",
|
||||
"1",
|
||||
).strip()
|
||||
|
||||
try:
|
||||
step_count = int(step_count_raw)
|
||||
except ValueError as exc:
|
||||
raise MethodServiceError(
|
||||
"Некорректное количество шагов."
|
||||
) from exc
|
||||
|
||||
if step_count < 1:
|
||||
raise MethodServiceError(
|
||||
"Метод должен содержать хотя бы один шаг."
|
||||
)
|
||||
|
||||
steps = []
|
||||
|
||||
for index in range(1, step_count + 1):
|
||||
prefix = f"step_{index}"
|
||||
|
||||
attack_mode_raw = request.form.get(
|
||||
f"{prefix}_attack_mode",
|
||||
"",
|
||||
).strip()
|
||||
|
||||
if not attack_mode_raw:
|
||||
raise MethodServiceError(
|
||||
f"Для шага {index} не выбран attack mode."
|
||||
)
|
||||
|
||||
try:
|
||||
attack_mode = int(attack_mode_raw)
|
||||
except ValueError as exc:
|
||||
raise MethodServiceError(
|
||||
f"Некорректный attack mode в шаге {index}."
|
||||
) from exc
|
||||
|
||||
if attack_mode not in (0, 1, 3, 6, 7):
|
||||
raise MethodServiceError(
|
||||
f"Attack mode {attack_mode} "
|
||||
f"не поддерживается клиентом CrackLab."
|
||||
)
|
||||
|
||||
step = {
|
||||
"step_id": f"step-{index:03d}",
|
||||
"attack_mode": attack_mode,
|
||||
}
|
||||
|
||||
workload_raw = request.form.get(
|
||||
f"{prefix}_workload_profile",
|
||||
"2",
|
||||
).strip()
|
||||
|
||||
if workload_raw:
|
||||
try:
|
||||
workload_profile = int(workload_raw)
|
||||
except ValueError as exc:
|
||||
raise MethodServiceError(
|
||||
f"Некорректный workload profile "
|
||||
f"в шаге {index}."
|
||||
) from exc
|
||||
|
||||
if workload_profile not in (1, 2, 3, 4):
|
||||
raise MethodServiceError(
|
||||
f"Workload profile в шаге {index} "
|
||||
"должен быть от 1 до 4."
|
||||
)
|
||||
|
||||
step["workload_profile"] = workload_profile
|
||||
|
||||
if attack_mode == 0:
|
||||
step["dictionary"] = {
|
||||
"resource": _get_dictionary(
|
||||
prefix,
|
||||
"dictionary",
|
||||
index,
|
||||
),
|
||||
}
|
||||
|
||||
elif attack_mode == 1:
|
||||
step["dictionaries"] = [
|
||||
{
|
||||
"resource": _get_dictionary(
|
||||
prefix,
|
||||
"dictionary_1",
|
||||
index,
|
||||
),
|
||||
},
|
||||
{
|
||||
"resource": _get_dictionary(
|
||||
prefix,
|
||||
"dictionary_2",
|
||||
index,
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
elif attack_mode == 3:
|
||||
step["mask"] = _get_mask(
|
||||
prefix,
|
||||
index,
|
||||
)
|
||||
|
||||
elif attack_mode == 6:
|
||||
step["dictionary"] = {
|
||||
"resource": _get_dictionary(
|
||||
prefix,
|
||||
"dictionary",
|
||||
index,
|
||||
),
|
||||
}
|
||||
step["mask"] = _get_mask(
|
||||
prefix,
|
||||
index,
|
||||
)
|
||||
|
||||
elif attack_mode == 7:
|
||||
step["mask"] = _get_mask(
|
||||
prefix,
|
||||
index,
|
||||
)
|
||||
step["dictionary"] = {
|
||||
"resource": _get_dictionary(
|
||||
prefix,
|
||||
"dictionary",
|
||||
index,
|
||||
),
|
||||
}
|
||||
|
||||
steps.append(step)
|
||||
|
||||
return steps
|
||||
|
||||
|
||||
def _get_dictionary(
|
||||
prefix: str,
|
||||
field: str,
|
||||
index: int,
|
||||
) -> str:
|
||||
value = request.form.get(
|
||||
f"{prefix}_{field}",
|
||||
"",
|
||||
).strip()
|
||||
|
||||
if not value:
|
||||
raise MethodServiceError(
|
||||
f"Для шага {index} необходимо указать словарь."
|
||||
)
|
||||
|
||||
if not value.startswith("wordlist:"):
|
||||
value = f"wordlist:{value}"
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _get_mask(
|
||||
prefix: str,
|
||||
index: int,
|
||||
) -> str:
|
||||
value = request.form.get(
|
||||
f"{prefix}_mask",
|
||||
"",
|
||||
).strip()
|
||||
|
||||
if not value:
|
||||
raise MethodServiceError(
|
||||
f"Для шага {index} необходимо указать маску."
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
@cracklab_bp.route("/clients/")
|
||||
def clients_page():
|
||||
return _placeholder(
|
||||
"Clients",
|
||||
"Client management will be implemented here.",
|
||||
"clients",
|
||||
)
|
||||
|
||||
|
||||
@cracklab_bp.route("/results/")
|
||||
def results_page():
|
||||
return _placeholder(
|
||||
"Results",
|
||||
"Result history will be implemented here.",
|
||||
"results",
|
||||
)
|
||||
|
||||
|
||||
def _placeholder(
|
||||
title: str,
|
||||
description: str,
|
||||
active_page: str,
|
||||
):
|
||||
return render_template(
|
||||
"placeholder.html",
|
||||
active_page=active_page,
|
||||
title=title,
|
||||
description=description,
|
||||
)
|
||||
@@ -0,0 +1,785 @@
|
||||
@font-face {
|
||||
font-family: "Bender";
|
||||
|
||||
src:
|
||||
url("fonts/Bender.woff2") format("woff2"),
|
||||
url("fonts/Bender.woff") format("woff");
|
||||
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: "Bender";
|
||||
|
||||
src:
|
||||
url("fonts/Bender-Bold.woff2") format("woff2"),
|
||||
url("fonts/Bender-Bold.woff") format("woff");
|
||||
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: "Bender";
|
||||
|
||||
src:
|
||||
url("fonts/Bender-Light.woff2") format("woff2"),
|
||||
url("fonts/Bender-Light.woff") format("woff");
|
||||
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
:root {
|
||||
--bg: #0f141a;
|
||||
--surface: #171e26;
|
||||
--surface-light: #1c252f;
|
||||
--border: #2b3743;
|
||||
|
||||
--text: #e6edf3;
|
||||
--muted: #8b98a6;
|
||||
|
||||
--green: #45d483;
|
||||
--yellow: #e5c07b;
|
||||
--red: #ef6262;
|
||||
--accent: #69a7ff;
|
||||
|
||||
--terminal: #0a0f14;
|
||||
}
|
||||
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
html {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
min-height: 100vh;
|
||||
|
||||
font-family:
|
||||
"Bender",
|
||||
Arial,
|
||||
Helvetica,
|
||||
sans-serif;
|
||||
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
a:hover {
|
||||
color: #9bc4ff;
|
||||
}
|
||||
|
||||
|
||||
.cracklab-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
z-index: 1000;
|
||||
|
||||
width: 100%;
|
||||
|
||||
background: rgba(15, 20, 26, 0.96);
|
||||
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
|
||||
.cracklab-header-inner {
|
||||
width: 100%;
|
||||
|
||||
max-width: 1800px;
|
||||
|
||||
margin: 0 auto;
|
||||
|
||||
min-height: 68px;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 36px;
|
||||
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
|
||||
.cracklab-brand {
|
||||
flex: 0 0 auto;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
color: var(--text);
|
||||
|
||||
text-decoration: none;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.cracklab-brand:hover {
|
||||
color: var(--text);
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
.cracklab-brand-title {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
|
||||
letter-spacing: 0.4px;
|
||||
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
|
||||
.cracklab-brand-subtitle {
|
||||
margin-top: 3px;
|
||||
|
||||
color: var(--muted);
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
letter-spacing: 0.7px;
|
||||
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
|
||||
.cracklab-nav {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 6px;
|
||||
|
||||
min-width: 0;
|
||||
|
||||
overflow-x: auto;
|
||||
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
|
||||
.cracklab-nav a {
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
min-height: 38px;
|
||||
|
||||
padding: 0 14px;
|
||||
|
||||
border-radius: 7px;
|
||||
|
||||
color: var(--muted);
|
||||
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
|
||||
letter-spacing: 0.7px;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
background 0.15s ease;
|
||||
}
|
||||
|
||||
|
||||
.cracklab-nav a:hover {
|
||||
color: var(--text);
|
||||
|
||||
background: var(--surface-light);
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
.cracklab-nav a.active {
|
||||
color: var(--text);
|
||||
|
||||
background: var(--surface-light);
|
||||
|
||||
box-shadow:
|
||||
inset 0 -2px 0 var(--accent);
|
||||
}
|
||||
|
||||
|
||||
.cracklab-content {
|
||||
width: 100%;
|
||||
|
||||
max-width: 1800px;
|
||||
|
||||
margin: 0 auto;
|
||||
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
|
||||
.page-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
|
||||
.page-description {
|
||||
color: var(--muted);
|
||||
|
||||
margin-bottom: 30px;
|
||||
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.methods-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.methods-page-header {
|
||||
display: flex;
|
||||
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
|
||||
gap: 24px;
|
||||
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
|
||||
.methods-page-header .page-description {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
.methods-button,
|
||||
.methods-button-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
min-height: 42px;
|
||||
padding: 10px 18px;
|
||||
|
||||
border: 2px outset ButtonFace;
|
||||
border-radius: 7px;
|
||||
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease,
|
||||
transform 0.15s ease;
|
||||
}
|
||||
|
||||
.methods-button {
|
||||
background: var(--accent);
|
||||
color: #0f141a;
|
||||
}
|
||||
|
||||
.methods-button:hover {
|
||||
background: #8abaff;
|
||||
color: #0f141a;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.methods-button:active {
|
||||
border-style: inset;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.methods-button-secondary {
|
||||
background: var(--surface-light);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.methods-button-secondary:hover {
|
||||
background: var(--border);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.methods-button-secondary:active {
|
||||
border-style: inset;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
|
||||
.methods-list {
|
||||
display: grid;
|
||||
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
|
||||
.method-card {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
gap: 30px;
|
||||
|
||||
padding: 20px 22px;
|
||||
|
||||
background: var(--surface);
|
||||
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
|
||||
box-shadow:
|
||||
0 2px 10px rgba(0, 0, 0, 0.20);
|
||||
}
|
||||
|
||||
|
||||
.method-card-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
.method-card-title {
|
||||
margin-bottom: 7px;
|
||||
|
||||
color: var(--text);
|
||||
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
.method-card-description {
|
||||
margin-bottom: 9px;
|
||||
|
||||
color: var(--muted);
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
.method-card-id {
|
||||
color: var(--muted);
|
||||
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
.method-card-meta {
|
||||
display: flex;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
|
||||
.method-meta-item {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
|
||||
.method-meta-item span {
|
||||
display: block;
|
||||
|
||||
margin-bottom: 5px;
|
||||
|
||||
color: var(--muted);
|
||||
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
.method-meta-item strong {
|
||||
color: var(--text);
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
.method-empty {
|
||||
padding: 50px 30px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
background: var(--surface);
|
||||
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
|
||||
.method-empty-title {
|
||||
margin-bottom: 10px;
|
||||
|
||||
color: var(--text);
|
||||
|
||||
font-size: 21px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
.method-empty-description {
|
||||
max-width: 520px;
|
||||
|
||||
margin: 0 auto 24px;
|
||||
|
||||
color: var(--muted);
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
|
||||
grid-template-columns:
|
||||
repeat(
|
||||
auto-fit,
|
||||
minmax(180px, 1fr)
|
||||
);
|
||||
|
||||
gap: 18px;
|
||||
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
|
||||
.stat-card {
|
||||
background: var(--surface);
|
||||
|
||||
border: 1px solid var(--border);
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
padding: 18px;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.stat-value {
|
||||
margin-bottom: 8px;
|
||||
|
||||
color: var(--accent);
|
||||
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
.stat-title {
|
||||
color: var(--muted);
|
||||
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
|
||||
grid-template-columns:
|
||||
minmax(260px, 0.35fr)
|
||||
minmax(500px, 1fr);
|
||||
|
||||
gap: 24px;
|
||||
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
|
||||
border: 1px solid var(--border);
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
padding: 22px;
|
||||
|
||||
margin-bottom: 24px;
|
||||
|
||||
box-shadow:
|
||||
0 2px 10px rgba(0, 0, 0, 0.20);
|
||||
}
|
||||
|
||||
|
||||
.card h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
|
||||
color: var(--text);
|
||||
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
|
||||
max-height: 700px;
|
||||
|
||||
border: 1px solid var(--border);
|
||||
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
|
||||
th {
|
||||
background: var(--surface-light);
|
||||
|
||||
color: var(--text);
|
||||
|
||||
font-weight: 700;
|
||||
|
||||
text-align: left;
|
||||
|
||||
padding: 12px;
|
||||
|
||||
border-bottom: 2px solid var(--border);
|
||||
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
||||
td {
|
||||
padding: 10px 12px;
|
||||
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
vertical-align: top;
|
||||
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
|
||||
tbody tr:hover {
|
||||
background: rgba(105, 167, 255, 0.05);
|
||||
}
|
||||
|
||||
|
||||
.status-list {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
padding-bottom: 10px;
|
||||
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
|
||||
.status-row:last-child {
|
||||
padding-bottom: 0;
|
||||
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
min-height: 24px;
|
||||
|
||||
padding: 0 9px;
|
||||
|
||||
border-radius: 6px;
|
||||
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
background: var(--surface-light);
|
||||
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
|
||||
.badge-created,
|
||||
.badge-queued {
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
|
||||
.badge-running,
|
||||
.badge-partial {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
|
||||
.badge-completed {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
|
||||
.badge-failed,
|
||||
.badge-cancelled {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
|
||||
.empty-state {
|
||||
padding: 35px 20px;
|
||||
|
||||
color: var(--muted);
|
||||
|
||||
text-align: center;
|
||||
|
||||
border: 1px dashed var(--border);
|
||||
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
.cracklab-header-inner {
|
||||
align-items: flex-start;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
|
||||
.cracklab-nav {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.cracklab-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.methods-page-header {
|
||||
align-items: stretch;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
|
||||
.methods-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.method-card {
|
||||
align-items: stretch;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
|
||||
.method-card-meta {
|
||||
width: 100%;
|
||||
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 600px) {
|
||||
|
||||
.cracklab-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
>
|
||||
|
||||
<title>
|
||||
{% block title %}
|
||||
CrackLab
|
||||
{% endblock %}
|
||||
</title>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{ url_for('cracklab.static', filename='cracklab.css') }}"
|
||||
>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="cracklab-header">
|
||||
|
||||
<div class="cracklab-header-inner">
|
||||
|
||||
<a
|
||||
class="cracklab-brand"
|
||||
href="{{ url_for('cracklab.dashboard') }}"
|
||||
>
|
||||
<span class="cracklab-brand-title">
|
||||
WiFi GPS Mapper
|
||||
</span>
|
||||
|
||||
<span class="cracklab-brand-subtitle">
|
||||
CrackLab
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav class="cracklab-nav">
|
||||
|
||||
<a
|
||||
class="{% if active_page == 'dashboard' %}active{% endif %}"
|
||||
href="{{ url_for('cracklab.dashboard') }}"
|
||||
>
|
||||
Dashboard
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="{% if active_page == 'jobs' %}active{% endif %}"
|
||||
href="{{ url_for('cracklab.jobs_page') }}"
|
||||
>
|
||||
Jobs
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="{% if active_page == 'methods' %}active{% endif %}"
|
||||
href="{{ url_for('cracklab.methods_page') }}"
|
||||
>
|
||||
Methods
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="{% if active_page == 'clients' %}active{% endif %}"
|
||||
href="{{ url_for('cracklab.clients_page') }}"
|
||||
>
|
||||
Clients
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="{% if active_page == 'results' %}active{% endif %}"
|
||||
href="{{ url_for('cracklab.results_page') }}"
|
||||
>
|
||||
Results
|
||||
</a>
|
||||
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<main class="cracklab-content">
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,215 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
CrackLab — Dashboard
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="page-title">
|
||||
CrackLab
|
||||
</h1>
|
||||
|
||||
<div class="page-description">
|
||||
Job and cracking coverage management.
|
||||
</div>
|
||||
|
||||
|
||||
<div class="stats-grid">
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">
|
||||
{{ methods_count }}
|
||||
</div>
|
||||
|
||||
<div class="stat-title">
|
||||
Methods
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">
|
||||
{{ jobs_count }}
|
||||
</div>
|
||||
|
||||
<div class="stat-title">
|
||||
Jobs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">
|
||||
{{ active_jobs_count }}
|
||||
</div>
|
||||
|
||||
<div class="stat-title">
|
||||
Active Jobs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">
|
||||
{{ clients_count }}
|
||||
</div>
|
||||
|
||||
<div class="stat-title">
|
||||
Clients
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">
|
||||
{{ results_count }}
|
||||
</div>
|
||||
|
||||
<div class="stat-title">
|
||||
Results
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="dashboard-grid">
|
||||
|
||||
<section class="card">
|
||||
|
||||
<h2>
|
||||
Job Status
|
||||
</h2>
|
||||
|
||||
<div class="status-list">
|
||||
|
||||
{% for status in (
|
||||
"CREATED",
|
||||
"QUEUED",
|
||||
"RUNNING",
|
||||
"PARTIAL",
|
||||
"COMPLETED",
|
||||
"FAILED",
|
||||
"CANCELLED"
|
||||
) %}
|
||||
|
||||
<div class="status-row">
|
||||
|
||||
<span>
|
||||
<span class="badge badge-{{ status|lower }}">
|
||||
{{ status }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<strong>
|
||||
{{ job_statuses.get(status, 0) }}
|
||||
</strong>
|
||||
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section class="card">
|
||||
|
||||
<h2>
|
||||
Recent Jobs
|
||||
</h2>
|
||||
|
||||
{% if recent_jobs %}
|
||||
|
||||
<div class="table-container">
|
||||
|
||||
<table>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Job</th>
|
||||
<th>Method</th>
|
||||
<th>Status</th>
|
||||
<th>Hashes</th>
|
||||
<th>Client</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
{% for job in recent_jobs %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<a
|
||||
href="{{ url_for(
|
||||
'cracklab.jobs_page'
|
||||
) }}"
|
||||
>
|
||||
{{ job["id"] }}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{ method_names.get(
|
||||
job["method_id"],
|
||||
job["method_id"]
|
||||
) }}
|
||||
<span class="muted">
|
||||
v{{ job["method_version"] }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span
|
||||
class="badge badge-{{
|
||||
job["status"]|lower
|
||||
}}"
|
||||
>
|
||||
{{ job["status"] }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{ job["hash_count"] }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if job["client_id"] %}
|
||||
{{ client_names.get(
|
||||
job["client_id"],
|
||||
job["client_id"]
|
||||
) }}
|
||||
{% else %}
|
||||
<span class="muted">
|
||||
—
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="empty-state">
|
||||
No jobs have been issued yet.
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
CrackLab — Methods
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="methods-page">
|
||||
|
||||
<div class="methods-page-header">
|
||||
|
||||
<div>
|
||||
<h1 class="page-title">
|
||||
Methods
|
||||
</h1>
|
||||
|
||||
<div class="page-description">
|
||||
Рабочие методы перебора, сохранённые в базе данных.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if methods %}
|
||||
<a
|
||||
class="methods-button"
|
||||
href="{{ url_for('cracklab.method_create_page') }}"
|
||||
>
|
||||
+ Создать метод
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% if methods %}
|
||||
|
||||
<section class="methods-list">
|
||||
|
||||
{% for method in methods %}
|
||||
|
||||
<article class="method-card">
|
||||
|
||||
<div class="method-card-main">
|
||||
|
||||
<div class="method-card-title">
|
||||
{{ method.name }}
|
||||
</div>
|
||||
|
||||
{% if method.description %}
|
||||
<div class="method-card-description">
|
||||
{{ method.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="method-card-id">
|
||||
{{ method.id }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="method-card-meta">
|
||||
|
||||
<div class="method-meta-item">
|
||||
<span>Version</span>
|
||||
|
||||
<strong>
|
||||
{% if method.version %}
|
||||
{{ method.version }}
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="method-meta-item">
|
||||
<span>Steps</span>
|
||||
|
||||
<strong>
|
||||
{{ method.steps_count }}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="method-meta-item">
|
||||
<span>Created</span>
|
||||
|
||||
<strong>
|
||||
{{ method.created_at }}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</section>
|
||||
|
||||
{% else %}
|
||||
|
||||
<section class="method-empty">
|
||||
|
||||
<div class="method-empty-title">
|
||||
Методов пока нет
|
||||
</div>
|
||||
|
||||
<div class="method-empty-description">
|
||||
Создайте первый метод, чтобы начать
|
||||
выдавать задания CrackLab.
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="methods-button"
|
||||
href="{{ url_for('cracklab.method_create_page') }}"
|
||||
>
|
||||
+ Создать метод
|
||||
</a>
|
||||
|
||||
</section>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
CrackLab — {{ title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="page-title">
|
||||
{{ title }}
|
||||
</h1>
|
||||
|
||||
<div class="page-description">
|
||||
{{ description }}
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
|
||||
<div class="empty-state">
|
||||
This section is under development.
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user