Initial commit: Veritas Suite Main Server and Core Engine
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
[target.x86_64-pc-windows-gnullvm]
|
||||
rustflags = [
|
||||
"-C", "link-arg=-static",
|
||||
"-C", "link-arg=-static-libgcc",
|
||||
"-C", "link-self-contained=yes"
|
||||
]
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/veritas-core",
|
||||
"crates/veritas-veritas/vre",
|
||||
"crates/veritas-veritas/vim",
|
||||
"crates/veritas-veritas/van",
|
||||
"crates/veritas-veritas/registry-explorer",
|
||||
"crates/veritas-aegis/auditor-wipe",
|
||||
"crates/veritas-aegis/ransomguard",
|
||||
"crates/veritas-aegis/triage",
|
||||
"crates/veritas-aegis/processguard",
|
||||
"crates/veritas-cura/one-click-clean",
|
||||
"crates/veritas-cura/easyrescue",
|
||||
"crates/veritas-cura/privacy-shield",
|
||||
"crates/veritas-cura/systemdoktor",
|
||||
"crates/veritas-cura/spooler-reset",
|
||||
"crates/veritas-cura/update-fix",
|
||||
"crates/veritas-cura/system-integrator",
|
||||
"crates/veritas-cura/port-doctor",
|
||||
"crates/veritas-cura/association-fix",
|
||||
"crates/veritas-server",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
authors = ["Veritas Suite Team"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
tokio = { version = "1.38", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
anyhow = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
sha2 = "0.10"
|
||||
rsa = "0.9"
|
||||
chrono = { version = "0.4", default-features = false, features = ["serde", "std", "clock"] }
|
||||
axum = "0.7"
|
||||
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid"] }
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "veritas-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
rsa.workspace = true
|
||||
chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
@@ -0,0 +1,17 @@
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Computes a unique hardware fingerprint (CPU / Machine identifier).
|
||||
pub fn generate_hardware_id() -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
let hostname = std::env::var("COMPUTERNAME")
|
||||
.or_else(|_| std::env::var("HOSTNAME"))
|
||||
.unwrap_or_else(|_| "UNKNOWN-HOST".to_string());
|
||||
|
||||
let username = std::env::var("USERNAME")
|
||||
.or_else(|_| std::env::var("USER"))
|
||||
.unwrap_or_else(|_| "UNKNOWN-USER".to_string());
|
||||
|
||||
let raw_id = format!("{}:{}", hostname, username);
|
||||
hasher.update(raw_id.as_bytes());
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod hwid;
|
||||
pub mod license;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ActivationRequest {
|
||||
pub license_key: String,
|
||||
pub hardware_id: String,
|
||||
pub app_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ActivationResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub signed_token: Option<String>,
|
||||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RegisterRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub user_id: Option<i32>,
|
||||
pub email: Option<String>,
|
||||
pub auth_token: Option<String>,
|
||||
pub is_admin: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserLicenseInfo {
|
||||
pub license_key: String,
|
||||
pub plan_name: String,
|
||||
pub max_devices: i32,
|
||||
pub activated_devices: i64,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserDashboardResponse {
|
||||
pub success: bool,
|
||||
pub email: String,
|
||||
pub licenses: Vec<UserLicenseInfo>,
|
||||
}
|
||||
|
||||
// Admin Panel Structures
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AdminUserItem {
|
||||
pub id: i32,
|
||||
pub email: String,
|
||||
pub is_admin: bool,
|
||||
pub created_at: String,
|
||||
pub license_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AdminLicenseItem {
|
||||
pub id: i32,
|
||||
pub user_id: Option<i32>,
|
||||
pub user_email: Option<String>,
|
||||
pub license_key: String,
|
||||
pub plan_name: String,
|
||||
pub max_devices: i32,
|
||||
pub activated_devices: i64,
|
||||
pub active: bool,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AdminStatsResponse {
|
||||
pub success: bool,
|
||||
pub total_users: i64,
|
||||
pub total_licenses: i64,
|
||||
pub total_activations: i64,
|
||||
pub users: Vec<AdminUserItem>,
|
||||
pub licenses: Vec<AdminLicenseItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateLicenseRequest {
|
||||
pub user_id: i32,
|
||||
pub plan_name: String,
|
||||
pub max_devices: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToggleLicenseRequest {
|
||||
pub license_key: String,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
// Download Center Structures
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SoftwareDownloadItem {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub category: String,
|
||||
pub file_size: String,
|
||||
pub download_url: String,
|
||||
pub description: String,
|
||||
pub is_available: bool,
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use crate::{hwid::generate_hardware_id, ActivationRequest};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub struct LicenseClient {
|
||||
pub server_url: String,
|
||||
pub app_name: String,
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
impl LicenseClient {
|
||||
pub fn new(server_url: impl Into<String>, app_name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
server_url: server_url.into(),
|
||||
app_name: app_name.into(),
|
||||
secret: "VERITAS_SECRET_KEY_SIGNING_2026".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_request(&self, license_key: &str) -> ActivationRequest {
|
||||
ActivationRequest {
|
||||
license_key: license_key.to_string(),
|
||||
hardware_id: generate_hardware_id(),
|
||||
app_name: self.app_name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_token_offline(&self, token: &str, license_key: &str, target_hwid: &str) -> bool {
|
||||
let parts: Vec<&str> = token.split(':').collect();
|
||||
if parts.len() != 4 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let token_key = parts[0];
|
||||
let token_hwid = parts[1];
|
||||
let token_exp: i64 = match parts[2].parse() {
|
||||
Ok(exp) => exp,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let token_sig = parts[3];
|
||||
|
||||
if token_key != license_key || token_hwid != target_hwid {
|
||||
return false;
|
||||
}
|
||||
|
||||
if chrono::Utc::now().timestamp() > token_exp {
|
||||
return false;
|
||||
}
|
||||
|
||||
let signature_input = format!(
|
||||
"{}:{}:{}:{}",
|
||||
token_key, token_hwid, token_exp, self.secret
|
||||
);
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(signature_input.as_bytes());
|
||||
let expected_sig = format!("{:x}", hasher.finalize());
|
||||
|
||||
token_sig == expected_sig
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_token_verification() {
|
||||
let client = LicenseClient::new("http://45.156.84.238", "VRE");
|
||||
let hwid = "WINDOWS-TEST-HWID-001";
|
||||
let key = "VERITAS-PRO-DEMO-2026";
|
||||
let exp = chrono::Utc::now().timestamp() + 3600;
|
||||
|
||||
let sig_input = format!("{}:{}:{}:{}", key, hwid, exp, client.secret);
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(sig_input.as_bytes());
|
||||
let sig = format!("{:x}", hasher.finalize());
|
||||
|
||||
let token = format!("{}:{}:{}:{}", key, hwid, exp, sig);
|
||||
|
||||
assert!(client.verify_token_offline(&token, key, hwid));
|
||||
assert!(!client.verify_token_offline(&token, "WRONG-KEY", hwid));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "veritas-server"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
veritas-core = { path = "../veritas-core" }
|
||||
tokio.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
rsa.workspace = true
|
||||
sha2.workspace = true
|
||||
sqlx = { workspace = true }
|
||||
tower-http = { version = "0.5", features = ["fs", "cors"] }
|
||||
@@ -0,0 +1,813 @@
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::{header, HeaderMap, HeaderValue, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{PgPool, Row};
|
||||
use std::{collections::HashMap, net::SocketAddr, path::Path, sync::Arc};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tower_http::services::ServeDir;
|
||||
use tracing_subscriber::fmt;
|
||||
use veritas_core::{
|
||||
ActivationRequest, ActivationResponse, AdminLicenseItem, AdminStatsResponse, AdminUserItem,
|
||||
AuthResponse, CreateLicenseRequest, LoginRequest, RegisterRequest, SoftwareDownloadItem,
|
||||
ToggleLicenseRequest, UserDashboardResponse, UserLicenseInfo,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub db: Option<PgPool>,
|
||||
pub secret: String,
|
||||
pub static_dir: String,
|
||||
pub rate_limiter: Arc<AsyncMutex<HashMap<String, (u32, i64)>>>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
fmt::init();
|
||||
|
||||
let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
||||
"postgres://veritas:veritas_db_password_2026!@localhost/veritas_db".to_string()
|
||||
});
|
||||
|
||||
let db_pool = match PgPool::connect(&db_url).await {
|
||||
Ok(pool) => {
|
||||
println!("✅ Connected to PostgreSQL database successfully.");
|
||||
Some(pool)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("⚠️ PostgreSQL connection failed ({}), running in fallback mode.", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let static_dir = if Path::new("crates/veritas-server/static").exists() {
|
||||
"crates/veritas-server/static".to_string()
|
||||
} else {
|
||||
"static".to_string()
|
||||
};
|
||||
|
||||
println!("📁 Serving Web Portal static files from: {}", static_dir);
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
db: db_pool,
|
||||
secret: "VERITAS_SECRET_KEY_SIGNING_2026_ENTERPRISE_HMAC".to_string(),
|
||||
static_dir,
|
||||
rate_limiter: Arc::new(AsyncMutex::new(HashMap::new())),
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
// Clean Pretty URLs (NO .html in URL bar)
|
||||
.route("/", get(serve_index))
|
||||
.route("/downloads", get(serve_downloads))
|
||||
.route("/portal", get(serve_portal))
|
||||
.route("/admin", get(serve_admin))
|
||||
// Redirect legacy .html URLs to clean URLs
|
||||
.route("/index.html", get(|| async { Redirect::permanent("/") }))
|
||||
.route("/downloads.html", get(|| async { Redirect::permanent("/downloads") }))
|
||||
.route("/portal.html", get(|| async { Redirect::permanent("/portal") }))
|
||||
.route("/admin.html", get(|| async { Redirect::permanent("/admin") }))
|
||||
// Core API Endpoints
|
||||
.route("/health", get(health_check))
|
||||
.route("/api/v1/activate", post(activate_license))
|
||||
.route("/api/v1/auth/register", post(register_user))
|
||||
.route("/api/v1/auth/login", post(login_user))
|
||||
.route("/api/v1/account/dashboard", get(get_dashboard))
|
||||
.route("/api/v1/downloads/list", get(get_downloads))
|
||||
// Server-Side Protected Admin Endpoints (Requires Admin JWT Token)
|
||||
.route("/api/v1/admin/stats", get(get_admin_stats))
|
||||
.route("/api/v1/admin/create-license", post(admin_create_license))
|
||||
.route("/api/v1/admin/toggle-license", post(admin_toggle_license))
|
||||
.fallback_service(ServeDir::new(&state.static_dir))
|
||||
.layer(middleware::from_fn(add_security_headers))
|
||||
.with_state(state);
|
||||
|
||||
let port: u16 = std::env::var("PORT")
|
||||
.unwrap_or_else(|_| "8080".to_string())
|
||||
.parse()
|
||||
.unwrap_or(8080);
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
println!("🌐 Veritas Suite Enterprise Secure Server running on http://{}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TASK 3: HTTP Security Headers Middleware
|
||||
async fn add_security_headers(req: axum::extract::Request, next: Next) -> Response {
|
||||
let mut res = next.run(req).await;
|
||||
let headers = res.headers_mut();
|
||||
|
||||
headers.insert(
|
||||
header::STRICT_TRANSPORT_SECURITY,
|
||||
HeaderValue::from_static("max-age=31536000; includeSubDomains; preload"),
|
||||
);
|
||||
headers.insert(
|
||||
header::X_FRAME_OPTIONS,
|
||||
HeaderValue::from_static("DENY"),
|
||||
);
|
||||
headers.insert(
|
||||
header::X_CONTENT_TYPE_OPTIONS,
|
||||
HeaderValue::from_static("nosniff"),
|
||||
);
|
||||
headers.insert(
|
||||
header::X_XSS_PROTECTION,
|
||||
HeaderValue::from_static("1; mode=block"),
|
||||
);
|
||||
headers.insert(
|
||||
header::CONTENT_SECURITY_POLICY,
|
||||
HeaderValue::from_static("default-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com;"),
|
||||
);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn serve_index(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let path = Path::new(&state.static_dir).join("index.html");
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(_) => (StatusCode::NOT_FOUND, "Index page not found").into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve_downloads(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let path = Path::new(&state.static_dir).join("downloads.html");
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(_) => (StatusCode::NOT_FOUND, "Downloads page not found").into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve_portal(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let path = Path::new(&state.static_dir).join("portal.html");
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(_) => (StatusCode::NOT_FOUND, "Portal page not found").into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve_admin(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let path = Path::new(&state.static_dir).join("admin.html");
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(_) => (StatusCode::NOT_FOUND, "Admin page not found").into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn health_check() -> &'static str {
|
||||
"OK - Veritas Suite Enterprise Secure Backend v1.4"
|
||||
}
|
||||
|
||||
fn hash_password(password: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(format!("VERITAS_SALT_2026:{}", password).as_bytes());
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
// TASK 2: Cryptographic JWT Generator & Validator with Expiration
|
||||
fn create_jwt_token(user_id: i32, email: &str, is_admin: bool, secret: &str) -> String {
|
||||
let exp = (chrono::Utc::now() + chrono::Duration::days(30)).timestamp();
|
||||
let payload = format!("{}:{}:{}:{}", user_id, email, is_admin, exp);
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(format!("{}:{}", payload, secret).as_bytes());
|
||||
let sig = format!("{:x}", hasher.finalize());
|
||||
|
||||
format!("{}.{}", payload, sig)
|
||||
}
|
||||
|
||||
fn verify_jwt_token(token: &str, secret: &str) -> Option<(i32, String, bool)> {
|
||||
let parts: Vec<&str> = token.split('.').collect();
|
||||
if parts.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let payload = parts[0];
|
||||
let sig = parts[1];
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(format!("{}:{}", payload, secret).as_bytes());
|
||||
let expected_sig = format!("{:x}", hasher.finalize());
|
||||
|
||||
if sig != expected_sig {
|
||||
return None;
|
||||
}
|
||||
|
||||
let fields: Vec<&str> = payload.split(':').collect();
|
||||
if fields.len() != 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let uid: i32 = fields[0].parse().ok()?;
|
||||
let email = fields[1].to_string();
|
||||
let is_admin: bool = fields[2].parse().ok()?;
|
||||
let exp: i64 = fields[3].parse().ok()?;
|
||||
|
||||
// Check expiration timestamp
|
||||
if chrono::Utc::now().timestamp() > exp {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((uid, email, is_admin))
|
||||
}
|
||||
|
||||
// Verify Admin Token Guard
|
||||
async fn verify_admin_token(headers: &HeaderMap, state: &AppState) -> bool {
|
||||
let auth_header = match headers.get("Authorization") {
|
||||
Some(val) => match val.to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
},
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let token = auth_header.trim_start_matches("Bearer ").trim();
|
||||
if token.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some((_uid, _email, is_admin)) = verify_jwt_token(token, &state.secret) {
|
||||
return is_admin;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
// TASK 1: API Rate Limiter Guard (Brute Force Protection)
|
||||
async fn check_rate_limit(state: &AppState, key: &str) -> bool {
|
||||
let mut limiter = state.rate_limiter.lock().await;
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
|
||||
let entry = limiter.entry(key.to_string()).or_insert((0, now));
|
||||
|
||||
// Reset window after 60 seconds
|
||||
if now - entry.1 > 60 {
|
||||
entry.0 = 0;
|
||||
entry.1 = now;
|
||||
}
|
||||
|
||||
entry.0 += 1;
|
||||
entry.0 <= 5 // Max 5 attempts per 60s
|
||||
}
|
||||
|
||||
async fn register_user(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<RegisterRequest>,
|
||||
) -> Json<AuthResponse> {
|
||||
if payload.email.trim().is_empty() || payload.password.trim().is_empty() {
|
||||
return Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Email and password are required.".to_string(),
|
||||
user_id: None,
|
||||
email: None,
|
||||
auth_token: None,
|
||||
is_admin: None,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(pool) = &state.db {
|
||||
let pass_hash = hash_password(&payload.password);
|
||||
let insert_res = sqlx::query(
|
||||
"INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id, is_admin"
|
||||
)
|
||||
.bind(&payload.email)
|
||||
.bind(&pass_hash)
|
||||
.fetch_one(pool)
|
||||
.await;
|
||||
|
||||
match insert_res {
|
||||
Ok(row) => {
|
||||
let user_id: i32 = row.get("id");
|
||||
let is_admin: bool = row.get("is_admin");
|
||||
|
||||
let lic_key = format!("VERITAS-PRO-{}-{}", user_id, &pass_hash[..8].to_uppercase());
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO licenses (user_id, license_key, plan_name, max_devices) VALUES ($1, $2, $3, $4)"
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(&lic_key)
|
||||
.bind("Veritas Suite Pro")
|
||||
.bind(5)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
let jwt = create_jwt_token(user_id, &payload.email, is_admin, &state.secret);
|
||||
|
||||
Json(AuthResponse {
|
||||
success: true,
|
||||
message: "User registered successfully. License key generated.".to_string(),
|
||||
user_id: Some(user_id),
|
||||
email: Some(payload.email),
|
||||
auth_token: Some(jwt),
|
||||
is_admin: Some(is_admin),
|
||||
})
|
||||
}
|
||||
Err(_) => Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Email already registered or registration failed.".to_string(),
|
||||
user_id: None,
|
||||
email: None,
|
||||
auth_token: None,
|
||||
is_admin: None,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Database unavailable.".to_string(),
|
||||
user_id: None,
|
||||
email: None,
|
||||
auth_token: None,
|
||||
is_admin: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_user(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<LoginRequest>,
|
||||
) -> Json<AuthResponse> {
|
||||
// Check Rate Limiting for IP / email combination
|
||||
if !check_rate_limit(&state, &payload.email).await {
|
||||
return Json(AuthResponse {
|
||||
success: false,
|
||||
message: "⚠️ Zu viele Anmeldeversuche. Bitte warte 60 Sekunden (Brute-Force Schutz).".to_string(),
|
||||
user_id: None,
|
||||
email: None,
|
||||
auth_token: None,
|
||||
is_admin: None,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(pool) = &state.db {
|
||||
let pass_hash = hash_password(&payload.password);
|
||||
let user_res = sqlx::query(
|
||||
"SELECT id, email, is_admin FROM users WHERE email = $1 AND password_hash = $2"
|
||||
)
|
||||
.bind(&payload.email)
|
||||
.bind(&pass_hash)
|
||||
.fetch_optional(pool)
|
||||
.await;
|
||||
|
||||
match user_res {
|
||||
Ok(Some(row)) => {
|
||||
let user_id: i32 = row.get("id");
|
||||
let email: String = row.get("email");
|
||||
let is_admin: bool = row.get("is_admin");
|
||||
let jwt = create_jwt_token(user_id, &email, is_admin, &state.secret);
|
||||
|
||||
Json(AuthResponse {
|
||||
success: true,
|
||||
message: "Login successful.".to_string(),
|
||||
user_id: Some(user_id),
|
||||
email: Some(email),
|
||||
auth_token: Some(jwt),
|
||||
is_admin: Some(is_admin),
|
||||
})
|
||||
}
|
||||
_ => Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Invalid email or password.".to_string(),
|
||||
user_id: None,
|
||||
email: None,
|
||||
auth_token: None,
|
||||
is_admin: None,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Database unavailable.".to_string(),
|
||||
user_id: None,
|
||||
email: None,
|
||||
auth_token: None,
|
||||
is_admin: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DashboardQuery {
|
||||
pub user_id: i32,
|
||||
}
|
||||
|
||||
async fn get_dashboard(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<DashboardQuery>,
|
||||
) -> Json<UserDashboardResponse> {
|
||||
if let Some(pool) = &state.db {
|
||||
let user_res = sqlx::query("SELECT email FROM users WHERE id = $1")
|
||||
.bind(query.user_id)
|
||||
.fetch_optional(pool)
|
||||
.await;
|
||||
|
||||
if let Ok(Some(u_row)) = user_res {
|
||||
let email: String = u_row.get("email");
|
||||
let lics = sqlx::query(
|
||||
"SELECT license_key, plan_name, max_devices, active FROM licenses WHERE user_id = $1"
|
||||
)
|
||||
.bind(query.user_id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut license_infos = Vec::new();
|
||||
for l in lics {
|
||||
let lic_key: String = l.get("license_key");
|
||||
let plan_name: String = l.get("plan_name");
|
||||
let max_devices: i32 = l.get("max_devices");
|
||||
let active: bool = l.get("active");
|
||||
|
||||
let count_res = sqlx::query(
|
||||
"SELECT COUNT(*) as count FROM activations WHERE license_key = $1"
|
||||
)
|
||||
.bind(&lic_key)
|
||||
.fetch_one(pool)
|
||||
.await;
|
||||
let count: i64 = count_res.map(|r| r.get("count")).unwrap_or(0);
|
||||
|
||||
license_infos.push(UserLicenseInfo {
|
||||
license_key: lic_key,
|
||||
plan_name,
|
||||
max_devices,
|
||||
activated_devices: count,
|
||||
active,
|
||||
});
|
||||
}
|
||||
|
||||
return Json(UserDashboardResponse {
|
||||
success: true,
|
||||
email,
|
||||
licenses: license_infos,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Json(UserDashboardResponse {
|
||||
success: false,
|
||||
email: "".to_string(),
|
||||
licenses: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_downloads() -> Json<Vec<SoftwareDownloadItem>> {
|
||||
let items = vec![
|
||||
SoftwareDownloadItem {
|
||||
name: "Veritas SpoolerReset".to_string(),
|
||||
version: "v1.2.0 (Commercial Edition)".to_string(),
|
||||
category: "Windows Fixer (Cura)".to_string(),
|
||||
file_size: "14.2 MB".to_string(),
|
||||
download_url: "/spooler-reset.exe".to_string(),
|
||||
description: "Autonomous Windows Druckspooler & Warteschlangen-Reparatur mit GPU GUI, UAC Elevation & VPS Lizenzprüfung.".to_string(),
|
||||
is_available: true,
|
||||
},
|
||||
SoftwareDownloadItem {
|
||||
name: "Veritas RansomGuard".to_string(),
|
||||
version: "v1.0.0 (Commercial Edition)".to_string(),
|
||||
category: "IT-Sicherheit (Aegis)".to_string(),
|
||||
file_size: "14.4 MB".to_string(),
|
||||
download_url: "/ransomguard.exe".to_string(),
|
||||
description: "Echtzeit Shannon-Entropie Scanner & Kanarienvogel-Köder gegen Krypto-Trojaner mit Autostart & VPS Aktivierung.".to_string(),
|
||||
is_available: true,
|
||||
},
|
||||
];
|
||||
Json(items)
|
||||
}
|
||||
|
||||
// Server-Side Protected Admin APIs (Requires valid Admin JWT token)
|
||||
async fn get_admin_stats(
|
||||
headers: HeaderMap,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Json<AdminStatsResponse> {
|
||||
if !verify_admin_token(&headers, &state).await {
|
||||
return Json(AdminStatsResponse {
|
||||
success: false,
|
||||
total_users: 0,
|
||||
total_licenses: 0,
|
||||
total_activations: 0,
|
||||
users: vec![],
|
||||
licenses: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(pool) = &state.db {
|
||||
let u_count: i64 = sqlx::query("SELECT COUNT(*) as count FROM users")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map(|r| r.get("count"))
|
||||
.unwrap_or(0);
|
||||
|
||||
let l_count: i64 = sqlx::query("SELECT COUNT(*) as count FROM licenses")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map(|r| r.get("count"))
|
||||
.unwrap_or(0);
|
||||
|
||||
let a_count: i64 = sqlx::query("SELECT COUNT(*) as count FROM activations")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map(|r| r.get("count"))
|
||||
.unwrap_or(0);
|
||||
|
||||
let users_rows = sqlx::query(
|
||||
"SELECT u.id, u.email, u.is_admin, u.created_at, COUNT(l.id) as l_count FROM users u LEFT JOIN licenses l ON u.id = l.user_id GROUP BY u.id, u.email, u.is_admin, u.created_at ORDER BY u.id DESC"
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut users_list = Vec::new();
|
||||
for r in users_rows {
|
||||
let created: chrono::NaiveDateTime = r.get("created_at");
|
||||
users_list.push(AdminUserItem {
|
||||
id: r.get("id"),
|
||||
email: r.get("email"),
|
||||
is_admin: r.get("is_admin"),
|
||||
created_at: created.format("%Y-%m-%d %H:%M").to_string(),
|
||||
license_count: r.get("l_count"),
|
||||
});
|
||||
}
|
||||
|
||||
let lics_rows = sqlx::query(
|
||||
"SELECT l.id, l.user_id, u.email as user_email, l.license_key, l.plan_name, l.max_devices, l.active, l.created_at FROM licenses l LEFT JOIN users u ON l.user_id = u.id ORDER BY l.id DESC"
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut lics_list = Vec::new();
|
||||
for r in lics_rows {
|
||||
let lic_key: String = r.get("license_key");
|
||||
let created: chrono::NaiveDateTime = r.get("created_at");
|
||||
|
||||
let count_res = sqlx::query("SELECT COUNT(*) as count FROM activations WHERE license_key = $1")
|
||||
.bind(&lic_key)
|
||||
.fetch_one(pool)
|
||||
.await;
|
||||
let act_count: i64 = count_res.map(|cr| cr.get("count")).unwrap_or(0);
|
||||
|
||||
lics_list.push(AdminLicenseItem {
|
||||
id: r.get("id"),
|
||||
user_id: r.get("user_id"),
|
||||
user_email: r.get("user_email"),
|
||||
license_key: lic_key,
|
||||
plan_name: r.get("plan_name"),
|
||||
max_devices: r.get("max_devices"),
|
||||
activated_devices: act_count,
|
||||
active: r.get("active"),
|
||||
created_at: created.format("%Y-%m-%d %H:%M").to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
return Json(AdminStatsResponse {
|
||||
success: true,
|
||||
total_users: u_count,
|
||||
total_licenses: l_count,
|
||||
total_activations: a_count,
|
||||
users: users_list,
|
||||
licenses: lics_list,
|
||||
});
|
||||
}
|
||||
|
||||
Json(AdminStatsResponse {
|
||||
success: false,
|
||||
total_users: 0,
|
||||
total_licenses: 0,
|
||||
total_activations: 0,
|
||||
users: vec![],
|
||||
licenses: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
async fn admin_create_license(
|
||||
headers: HeaderMap,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<CreateLicenseRequest>,
|
||||
) -> Json<ActivationResponse> {
|
||||
if !verify_admin_token(&headers, &state).await {
|
||||
return Json(ActivationResponse {
|
||||
success: false,
|
||||
message: "Unauthorized: Admin privileges required.".to_string(),
|
||||
signed_token: None,
|
||||
expires_at: None,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(pool) = &state.db {
|
||||
let lic_key = format!(
|
||||
"VERITAS-{}-{}-{}",
|
||||
payload.plan_name.to_uppercase().replace(" ", ""),
|
||||
payload.user_id,
|
||||
&sha2::Sha256::digest(chrono::Utc::now().to_string().as_bytes())[..4]
|
||||
.iter()
|
||||
.map(|b| format!("{:02X}", b))
|
||||
.collect::<String>()
|
||||
);
|
||||
|
||||
let res = sqlx::query(
|
||||
"INSERT INTO licenses (user_id, license_key, plan_name, max_devices) VALUES ($1, $2, $3, $4)"
|
||||
)
|
||||
.bind(payload.user_id)
|
||||
.bind(&lic_key)
|
||||
.bind(&payload.plan_name)
|
||||
.bind(payload.max_devices)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
if res.is_ok() {
|
||||
return Json(ActivationResponse {
|
||||
success: true,
|
||||
message: format!("License created: {}", lic_key),
|
||||
signed_token: Some(lic_key),
|
||||
expires_at: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Json(ActivationResponse {
|
||||
success: false,
|
||||
message: "Failed to create license.".to_string(),
|
||||
signed_token: None,
|
||||
expires_at: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn admin_toggle_license(
|
||||
headers: HeaderMap,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<ToggleLicenseRequest>,
|
||||
) -> Json<ActivationResponse> {
|
||||
if !verify_admin_token(&headers, &state).await {
|
||||
return Json(ActivationResponse {
|
||||
success: false,
|
||||
message: "Unauthorized: Admin privileges required.".to_string(),
|
||||
signed_token: None,
|
||||
expires_at: None,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(pool) = &state.db {
|
||||
let res = sqlx::query("UPDATE licenses SET active = $1 WHERE license_key = $2")
|
||||
.bind(payload.active)
|
||||
.bind(&payload.license_key)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
if res.is_ok() {
|
||||
return Json(ActivationResponse {
|
||||
success: true,
|
||||
message: format!("License active state set to {}", payload.active),
|
||||
signed_token: None,
|
||||
expires_at: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Json(ActivationResponse {
|
||||
success: false,
|
||||
message: "Failed to update license state.".to_string(),
|
||||
signed_token: None,
|
||||
expires_at: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn activate_license(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<ActivationRequest>,
|
||||
) -> Json<ActivationResponse> {
|
||||
tracing::info!(
|
||||
"Activation request: key={}, hwid={}, app={}",
|
||||
payload.license_key,
|
||||
payload.hardware_id,
|
||||
payload.app_name
|
||||
);
|
||||
|
||||
if payload.license_key.trim().is_empty() || payload.hardware_id.trim().is_empty() {
|
||||
return Json(ActivationResponse {
|
||||
success: false,
|
||||
message: "License key and Hardware ID are required.".to_string(),
|
||||
signed_token: None,
|
||||
expires_at: None,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(pool) = &state.db {
|
||||
let lic_result = sqlx::query(
|
||||
"SELECT max_devices, active FROM licenses WHERE license_key = $1"
|
||||
)
|
||||
.bind(&payload.license_key)
|
||||
.fetch_optional(pool)
|
||||
.await;
|
||||
|
||||
match lic_result {
|
||||
Ok(Some(row)) => {
|
||||
let active: bool = row.get("active");
|
||||
let max_devices: i32 = row.get("max_devices");
|
||||
|
||||
if !active {
|
||||
return Json(ActivationResponse {
|
||||
success: false,
|
||||
message: "License key has been deactivated.".to_string(),
|
||||
signed_token: None,
|
||||
expires_at: None,
|
||||
});
|
||||
}
|
||||
|
||||
let count_result = sqlx::query(
|
||||
"SELECT COUNT(*) as count FROM activations WHERE license_key = $1"
|
||||
)
|
||||
.bind(&payload.license_key)
|
||||
.fetch_one(pool)
|
||||
.await;
|
||||
|
||||
let activated_count: i64 = count_result.map(|r| r.get("count")).unwrap_or(0);
|
||||
let max_devices = max_devices as i64;
|
||||
|
||||
let is_already_active = sqlx::query(
|
||||
"SELECT id FROM activations WHERE license_key = $1 AND hardware_id = $2"
|
||||
)
|
||||
.bind(&payload.license_key)
|
||||
.bind(&payload.hardware_id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
|
||||
if !is_already_active && activated_count >= max_devices {
|
||||
return Json(ActivationResponse {
|
||||
success: false,
|
||||
message: format!(
|
||||
"Device limit reached ({}/{} max devices).",
|
||||
activated_count, max_devices
|
||||
),
|
||||
signed_token: None,
|
||||
expires_at: None,
|
||||
});
|
||||
}
|
||||
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO activations (license_key, hardware_id, app_name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING"
|
||||
)
|
||||
.bind(&payload.license_key)
|
||||
.bind(&payload.hardware_id)
|
||||
.bind(&payload.app_name)
|
||||
.execute(pool)
|
||||
.await;
|
||||
}
|
||||
Ok(None) => {
|
||||
return Json(ActivationResponse {
|
||||
success: false,
|
||||
message: "Invalid license key.".to_string(),
|
||||
signed_token: None,
|
||||
expires_at: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("DB Query Error: {}", e);
|
||||
return Json(ActivationResponse {
|
||||
success: false,
|
||||
message: "Internal server error validating license.".to_string(),
|
||||
signed_token: None,
|
||||
expires_at: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let expires = chrono::Utc::now() + chrono::Duration::days(365);
|
||||
let signature_input = format!(
|
||||
"{}:{}:{}:{}",
|
||||
payload.license_key,
|
||||
payload.hardware_id,
|
||||
expires.timestamp(),
|
||||
state.secret
|
||||
);
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(signature_input.as_bytes());
|
||||
let sig_hex = format!("{:x}", hasher.finalize());
|
||||
|
||||
let token = format!(
|
||||
"{}:{}:{}:{}",
|
||||
payload.license_key,
|
||||
payload.hardware_id,
|
||||
expires.timestamp(),
|
||||
sig_hex
|
||||
);
|
||||
|
||||
Json(ActivationResponse {
|
||||
success: true,
|
||||
message: "License activated successfully.".to_string(),
|
||||
signed_token: Some(token),
|
||||
expires_at: Some(expires),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Administrator Kontrollzentrum | Veritas Suite</title>
|
||||
<meta name="description" content="Geschütztes Administrator-Portal der Veritas Suite: B2B-Statistiken, Benutzerverwaltung, Lizenzerstellung und Geräte-Logs.">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
|
||||
<!-- Background Orbs -->
|
||||
<div class="glow-orb orb-purple"></div>
|
||||
<div class="glow-orb orb-cyan" style="background: radial-gradient(circle, rgba(245, 158, 11, 0.15) 0%, rgba(0, 0, 0, 0) 70%);"></div>
|
||||
|
||||
<!-- Header Navigation -->
|
||||
<header class="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="/" class="logo">
|
||||
<div class="logo-shield" style="background: linear-gradient(135deg, #f59e0b, #b45309);">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
||||
</div>
|
||||
<div class="logo-brand">
|
||||
<span class="brand-name" style="color: #fbbf24;">VERITAS ADMIN</span>
|
||||
<span class="brand-tag" style="background: rgba(245, 158, 11, 0.2); color: #fef08a;">CONTROL CENTER</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<nav class="nav-links">
|
||||
<a href="/" class="nav-item">🌐 Startseite</a>
|
||||
<a href="/downloads" class="nav-item">📥 Downloads</a>
|
||||
<a href="/portal" class="nav-item">👤 Kundenportal</a>
|
||||
<a href="https://mail.kanjiv.at/SOGo/index/" target="_blank" class="nav-item" style="color: #38bdf8;">📧 E-Mail Dienst</a>
|
||||
<a href="https://git.kanjiv.at/" target="_blank" class="nav-item" style="color: #a855f7;">🐙 Git Server</a>
|
||||
<a href="/admin" class="nav-item active" style="color: #fbbf24; border-bottom: 2px solid #f59e0b;">👑 Admin-Zentrale</a>
|
||||
</nav>
|
||||
|
||||
<div class="server-status-pill" id="status-pill">
|
||||
<span class="status-indicator"></span>
|
||||
<span id="status-text">Server: Verbinde...</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Admin Section -->
|
||||
<section class="section" style="padding-top: 7rem;">
|
||||
<div class="container">
|
||||
|
||||
<!-- Global Alert Box -->
|
||||
<div id="admin-alert" class="alert-box" style="display: none; margin-bottom: 1.5rem; padding: 12px 16px; border-radius: 8px; font-weight: 500;"></div>
|
||||
|
||||
<!-- STATE 1: ACCESS DENIED CARD (If not logged in as Admin in Customer Portal) -->
|
||||
<div id="admin-logged-out-state" class="portal-card glass-panel" style="max-width: 520px; margin: 3rem auto; padding: 2.5rem; border-radius: 16px; border: 1px solid rgba(245, 158, 11, 0.3); background: rgba(24, 34, 53, 0.85); text-align: center;">
|
||||
<div style="font-size: 3.5rem; margin-bottom: 1rem;">🔒</div>
|
||||
<h2 style="color: #fbbf24; margin-bottom: 0.5rem;">Zugriff geschützt</h2>
|
||||
<p style="color: #cbd5e1; font-size: 0.95rem; line-height: 1.6; margin-bottom: 1.8rem;">
|
||||
Die Admin-Zentrale ist geschützt. Bitte melde dich zuerst im <strong>Kundenportal</strong> mit deinem Administrator-Konto an.
|
||||
</p>
|
||||
<a href="/portal" class="btn btn-glow btn-block" style="display: inline-block; background: linear-gradient(135deg, #f59e0b, #b45309); color: #fff; font-weight: 700; padding: 12px 24px; border-radius: 8px; text-decoration: none;">
|
||||
👤 Zum Kundenportal & Admin-Login
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- STATE 2: FULL ADMIN CONTROL DASHBOARD (If logged in as Admin in Customer Portal) -->
|
||||
<div id="admin-logged-in-state" style="display: none;">
|
||||
|
||||
<!-- Admin Header Bar -->
|
||||
<div class="dash-user-bar glass-panel" style="display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.5rem; border-radius: 12px; background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); margin-bottom: 2rem;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<span style="font-size: 2rem;">👑</span>
|
||||
<div>
|
||||
<span style="color: #fef08a; font-size: 0.8rem; font-weight: 700; text-transform: uppercase;">Administrator Kontrollzentrum</span>
|
||||
<h3 id="admin-user-email-display" style="color: #fff; margin: 0;">admin@kanjiv.at</h3>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-outline-danger" onclick="logoutAdmin()">Admin Abmelden</button>
|
||||
</div>
|
||||
|
||||
<!-- KPI Metrics Cards -->
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.2rem; margin-bottom: 2rem;">
|
||||
<div class="glass-panel" style="padding: 1.5rem; border-radius: 12px; background: rgba(24, 34, 53, 0.7); text-align: center; border: 1px solid rgba(255,255,255,0.08);">
|
||||
<span style="font-size: 2.2rem; font-weight: 800; color: #fff;" id="admin-stat-users">0</span>
|
||||
<span style="display: block; color: #94a3b8; font-size: 0.85rem; margin-top: 4px;">👥 Registrierte Benutzer</span>
|
||||
</div>
|
||||
<div class="glass-panel" style="padding: 1.5rem; border-radius: 12px; background: rgba(24, 34, 53, 0.7); text-align: center; border: 1px solid rgba(255,255,255,0.08);">
|
||||
<span style="font-size: 2.2rem; font-weight: 800; color: #38bdf8;" id="admin-stat-licenses">0</span>
|
||||
<span style="display: block; color: #94a3b8; font-size: 0.85rem; margin-top: 4px;">🔑 Aktive Lizenzen</span>
|
||||
</div>
|
||||
<div class="glass-panel" style="padding: 1.5rem; border-radius: 12px; background: rgba(24, 34, 53, 0.7); text-align: center; border: 1px solid rgba(255,255,255,0.08);">
|
||||
<span style="font-size: 2.2rem; font-weight: 800; color: #34d399;" id="admin-stat-activations">0</span>
|
||||
<span style="display: block; color: #94a3b8; font-size: 0.85rem; margin-top: 4px;">⚡ Geräte-Aktivierungen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create License Form Card -->
|
||||
<div class="glass-panel" style="padding: 1.8rem; border-radius: 14px; background: rgba(24, 34, 53, 0.8); border: 1px solid rgba(245, 158, 11, 0.3); margin-bottom: 2rem;">
|
||||
<h3 style="color: #fbbf24; margin-bottom: 1rem; font-size: 1.2rem;">➕ Neue Manuelle B2B-Lizenz Erstellen</h3>
|
||||
<form onsubmit="handleAdminCreateLicense(event)" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.2rem; align-items: end;">
|
||||
<div class="form-group">
|
||||
<label style="display: block; margin-bottom: 6px; color: #cbd5e1; font-size: 0.9rem;">Benutzer-ID (Ziel-User)</label>
|
||||
<input type="number" id="admin-create-uid" value="1" required style="width: 100%; padding: 10px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(3, 7, 18, 0.8); color: #fff;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="display: block; margin-bottom: 6px; color: #cbd5e1; font-size: 0.9rem;">Plan-Bezeichnung</label>
|
||||
<select id="admin-create-plan" style="width: 100%; padding: 10px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(3, 7, 18, 0.8); color: #fff;">
|
||||
<option value="Veritas Suite Pro">Veritas Suite Pro (5 Geräte)</option>
|
||||
<option value="Veritas Enterprise Forensics">Veritas Enterprise Forensics (25 Geräte)</option>
|
||||
<option value="Veritas Ultimate Care">Veritas Ultimate Care (10 Geräte)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="display: block; margin-bottom: 6px; color: #cbd5e1; font-size: 0.9rem;">Max Geräte-Slots</label>
|
||||
<input type="number" id="admin-create-max" value="5" required style="width: 100%; padding: 10px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(3, 7, 18, 0.8); color: #fff;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-emerald" style="width: 100%; padding: 10px;">🔑 Lizenz Schlüssel Generieren</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Admin System Licenses Table -->
|
||||
<div class="glass-panel" style="padding: 1.8rem; border-radius: 14px; background: rgba(24, 34, 53, 0.8); border: 1px solid rgba(255,255,255,0.08);">
|
||||
<h3 style="color: #fff; margin-bottom: 1rem; font-size: 1.2rem;">📋 Alle Lizenzen & Sperr-Status im System</h3>
|
||||
<div style="overflow-x: auto;">
|
||||
<table style="width: 100%; border-collapse: collapse; text-align: left;">
|
||||
<thead>
|
||||
<tr style="background: rgba(3, 7, 18, 0.8); color: #94a3b8; font-size: 0.9rem;">
|
||||
<th style="padding: 12px;">ID</th>
|
||||
<th style="padding: 12px;">Lizenzschlüssel</th>
|
||||
<th style="padding: 12px;">Kunden E-Mail</th>
|
||||
<th style="padding: 12px;">Plan</th>
|
||||
<th style="padding: 12px;">Geräte</th>
|
||||
<th style="padding: 12px;">Status</th>
|
||||
<th style="padding: 12px;">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="admin-licenses-table-body" style="color: #cbd5e1;">
|
||||
<tr><td colspan="7" style="padding: 16px; text-align: center;">Lade Systemdaten...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container footer-content">
|
||||
<div class="footer-brand">
|
||||
<span class="logo-shield" style="background: linear-gradient(135deg, #f59e0b, #b45309);">🛡️</span>
|
||||
<span style="color: #fbbf24;">VERITAS SUITE ADMIN</span>
|
||||
</div>
|
||||
<p>© 2026 Veritas Suite. Geschütztes Administrator-Portal auf VPS kanjiv.at.</p>
|
||||
<div class="footer-links">
|
||||
<a href="/">Startseite</a>
|
||||
<a href="/downloads">Downloads</a>
|
||||
<a href="/portal">Kundenportal</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,472 @@
|
||||
// Veritas Suite Web Portal App Logic
|
||||
|
||||
const API_BASE = "";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
checkServerHealth();
|
||||
restoreSession();
|
||||
fetchDownloads();
|
||||
|
||||
// Check if we are on admin page
|
||||
if (window.location.pathname === "/admin" || window.location.pathname.endsWith("admin.html")) {
|
||||
checkAdminPageSession();
|
||||
}
|
||||
});
|
||||
|
||||
// Check Server Connection
|
||||
async function checkServerHealth() {
|
||||
const indicator = document.querySelector(".status-indicator");
|
||||
const statusText = document.getElementById("status-text");
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/health`);
|
||||
if (response.ok) {
|
||||
if (indicator) indicator.classList.add("online");
|
||||
if (statusText) statusText.textContent = "VPS Server: Online (200 OK)";
|
||||
} else {
|
||||
if (statusText) statusText.textContent = "VPS Server: Fehler";
|
||||
}
|
||||
} catch (e) {
|
||||
if (statusText) statusText.textContent = "VPS Server: Offline";
|
||||
}
|
||||
}
|
||||
|
||||
// Filter Sparten Tools
|
||||
function filterSparten(category) {
|
||||
document.querySelectorAll(".filter-btn").forEach(btn => btn.classList.remove("active"));
|
||||
|
||||
const activeBtn = Array.from(document.querySelectorAll(".filter-btn")).find(btn =>
|
||||
btn.getAttribute("onclick") && btn.getAttribute("onclick").includes(`'${category}'`)
|
||||
);
|
||||
if (activeBtn) activeBtn.classList.add("active");
|
||||
|
||||
const cards = document.querySelectorAll(".tool-card");
|
||||
cards.forEach(card => {
|
||||
if (category === "all" || card.getAttribute("data-sparte") === category) {
|
||||
card.style.display = "flex";
|
||||
} else {
|
||||
card.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Restore Session from LocalStorage
|
||||
function restoreSession() {
|
||||
const sessionStr = localStorage.getItem("veritas_session");
|
||||
if (sessionStr) {
|
||||
try {
|
||||
const session = JSON.parse(sessionStr);
|
||||
if (session && session.user_id) {
|
||||
showLoggedInState(session);
|
||||
} else {
|
||||
showLoggedOutState();
|
||||
}
|
||||
} catch (e) {
|
||||
localStorage.removeItem("veritas_session");
|
||||
showLoggedOutState();
|
||||
}
|
||||
} else {
|
||||
showLoggedOutState();
|
||||
}
|
||||
}
|
||||
|
||||
function showLoggedInState(session) {
|
||||
const loggedOutDiv = document.getElementById("dash-logged-out");
|
||||
const loggedInDiv = document.getElementById("dash-logged-in");
|
||||
const userEmailSpan = document.getElementById("dash-user-email");
|
||||
const userStatusBar = document.getElementById("user-status-bar");
|
||||
const authForms = document.getElementById("auth-forms-container");
|
||||
const navAdminLink = document.getElementById("nav-admin-link");
|
||||
const btnAdminPanel = document.getElementById("btn-admin-panel");
|
||||
|
||||
if (loggedOutDiv) loggedOutDiv.style.display = "none";
|
||||
if (loggedInDiv) loggedInDiv.style.display = "block";
|
||||
if (userEmailSpan) userEmailSpan.textContent = session.email;
|
||||
if (userStatusBar) userStatusBar.style.display = "inline-flex";
|
||||
if (authForms) authForms.style.display = "none";
|
||||
|
||||
// Show Admin Buttons ONLY if logged-in user is an Admin
|
||||
if (session.is_admin) {
|
||||
if (navAdminLink) navAdminLink.style.display = "inline-flex";
|
||||
if (btnAdminPanel) btnAdminPanel.style.display = "inline-block";
|
||||
} else {
|
||||
if (navAdminLink) navAdminLink.style.display = "none";
|
||||
if (btnAdminPanel) btnAdminPanel.style.display = "none";
|
||||
}
|
||||
|
||||
refreshDashboard();
|
||||
}
|
||||
|
||||
function showLoggedOutState() {
|
||||
const loggedOutDiv = document.getElementById("dash-logged-out");
|
||||
const loggedInDiv = document.getElementById("dash-logged-in");
|
||||
const userStatusBar = document.getElementById("user-status-bar");
|
||||
const authForms = document.getElementById("auth-forms-container");
|
||||
const navAdminLink = document.getElementById("nav-admin-link");
|
||||
const btnAdminPanel = document.getElementById("btn-admin-panel");
|
||||
|
||||
if (loggedOutDiv) loggedOutDiv.style.display = "block";
|
||||
if (loggedInDiv) loggedInDiv.style.display = "none";
|
||||
if (userStatusBar) userStatusBar.style.display = "none";
|
||||
if (authForms) authForms.style.display = "grid";
|
||||
if (navAdminLink) navAdminLink.style.display = "none";
|
||||
if (btnAdminPanel) btnAdminPanel.style.display = "none";
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem("veritas_session");
|
||||
showLoggedOutState();
|
||||
showAlert("Erfolgreich abgemeldet.", "success");
|
||||
if (window.location.pathname === "/admin" || window.location.pathname.endsWith("admin.html")) {
|
||||
window.location.href = "/portal";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Customer Registration
|
||||
async function handleRegister(event) {
|
||||
event.preventDefault();
|
||||
const email = document.getElementById("reg-email").value;
|
||||
const password = document.getElementById("reg-password").value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/auth/register`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showAlert(data.message, "success");
|
||||
const session = { user_id: data.user_id, email: data.email, token: data.auth_token, is_admin: data.is_admin };
|
||||
localStorage.setItem("veritas_session", JSON.stringify(session));
|
||||
showLoggedInState(session);
|
||||
} else {
|
||||
showAlert(data.message, "error");
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert("Netzwerkfehler bei der Registrierung.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Customer & Admin Login in Portal
|
||||
async function handleLogin(event) {
|
||||
event.preventDefault();
|
||||
const email = document.getElementById("login-email").value;
|
||||
const password = document.getElementById("login-password").value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showAlert(data.message, "success");
|
||||
const session = { user_id: data.user_id, email: data.email, token: data.auth_token, is_admin: data.is_admin };
|
||||
localStorage.setItem("veritas_session", JSON.stringify(session));
|
||||
showLoggedInState(session);
|
||||
|
||||
if (data.is_admin) {
|
||||
showAlert("👑 Willkommen Admin! Du hast Zugriff auf die Admin-Zentrale.", "success");
|
||||
}
|
||||
} else {
|
||||
showAlert(data.message, "error");
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert("Netzwerkfehler beim Anmelden.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Check Admin Page Session (Requires Admin Login in Customer Portal)
|
||||
function checkAdminPageSession() {
|
||||
const sessionStr = localStorage.getItem("veritas_session");
|
||||
const loggedOutState = document.getElementById("admin-logged-out-state");
|
||||
const loggedInState = document.getElementById("admin-logged-in-state");
|
||||
const userEmailDisplay = document.getElementById("admin-user-email-display");
|
||||
|
||||
if (sessionStr) {
|
||||
try {
|
||||
const session = JSON.parse(sessionStr);
|
||||
if (session && session.is_admin) {
|
||||
if (loggedOutState) loggedOutState.style.display = "none";
|
||||
if (loggedInState) loggedInState.style.display = "block";
|
||||
if (userEmailDisplay) userEmailDisplay.textContent = session.email;
|
||||
fetchAdminStats();
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (loggedOutState) loggedOutState.style.display = "block";
|
||||
if (loggedInState) loggedInState.style.display = "none";
|
||||
}
|
||||
|
||||
function logoutAdmin() {
|
||||
logout();
|
||||
}
|
||||
|
||||
// Refresh Customer Dashboard Data
|
||||
async function refreshDashboard() {
|
||||
const sessionStr = localStorage.getItem("veritas_session");
|
||||
if (!sessionStr) return;
|
||||
|
||||
const session = JSON.parse(sessionStr);
|
||||
const container = document.getElementById("license-list");
|
||||
if (!container) return;
|
||||
container.innerHTML = "<p style='color:#94a3b8;'>Lizenzen werden geladen...</p>";
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/account/dashboard?user_id=${session.user_id}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.licenses && data.licenses.length > 0) {
|
||||
container.innerHTML = "";
|
||||
data.licenses.forEach(lic => {
|
||||
const percent = Math.min(100, (lic.activated_devices / lic.max_devices) * 100);
|
||||
const cardHtml = `
|
||||
<div class="lic-card glass-panel" style="background: rgba(24, 34, 53, 0.7); padding: 1.25rem; border-radius: 12px; border: 1px solid rgba(255,255,255,0.08); margin-bottom: 1rem;">
|
||||
<div class="lic-top" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
|
||||
<span class="lic-plan" style="font-weight: 700; color: #fff; font-size: 1.1rem;">🛡️ ${lic.plan_name}</span>
|
||||
<span class="status-badge ${lic.active ? 'badge-active' : 'badge-planned'}" style="padding: 4px 10px; border-radius: 6px; font-size: 0.8rem; font-weight: 600;">${lic.active ? '🟢 AKTIV' : '🔴 GESPERRT'}</span>
|
||||
</div>
|
||||
<div class="lic-key-row" style="display: flex; align-items: center; justify-content: space-between; background: rgba(3, 7, 18, 0.6); padding: 10px 14px; border-radius: 8px; margin-bottom: 0.75rem;">
|
||||
<code style="font-family: monospace; color: #38bdf8; font-size: 1.05rem;">${lic.license_key}</code>
|
||||
<button class="btn btn-glass btn-small" onclick="copyKey('${lic.license_key}')">📋 Kopieren</button>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||
<small style="color: #94a3b8;">Aktivierte Geräte</small>
|
||||
<small style="color: #6366f1; font-weight: 600;">${lic.activated_devices} / ${lic.max_devices} Geräte</small>
|
||||
</div>
|
||||
<div class="progress-bg" style="background: rgba(255,255,255,0.1); height: 8px; border-radius: 4px; overflow: hidden;">
|
||||
<div class="progress-fill" style="width: ${percent}%; background: linear-gradient(90deg, #6366f1, #34d399); height: 100%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.innerHTML += cardHtml;
|
||||
|
||||
const simKeyInput = document.getElementById("sim-key");
|
||||
if (simKeyInput) simKeyInput.value = lic.license_key;
|
||||
});
|
||||
} else {
|
||||
container.innerHTML = "<p style='color:#94a3b8;'>Keine aktiven Lizenzen für dieses Konto gefunden.</p>";
|
||||
}
|
||||
} catch (e) {
|
||||
container.innerHTML = "<p class='error' style='color:#f87171;'>Fehler beim Laden des Dashboards.</p>";
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Software Downloads
|
||||
async function fetchDownloads() {
|
||||
const container = document.getElementById("downloads-container");
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/downloads/list`);
|
||||
const items = await response.json();
|
||||
|
||||
container.innerHTML = "";
|
||||
items.forEach(item => {
|
||||
const card = `
|
||||
<div class="tool-card glass-panel">
|
||||
<div class="card-top">
|
||||
<span class="sparte-tag tag-cura">${item.category}</span>
|
||||
<span class="status-badge ${item.is_available ? 'badge-active' : 'badge-planned'}">${item.version}</span>
|
||||
</div>
|
||||
<h3>${item.name}</h3>
|
||||
<p class="tool-desc">${item.description}</p>
|
||||
<div class="card-footer" style="justify-content: space-between; align-items: center;">
|
||||
<span class="tech-tag">Größe: ${item.file_size}</span>
|
||||
${item.is_available ?
|
||||
`<a class="btn btn-emerald btn-small" href="${item.download_url}" download>📥 Download .exe</a>` :
|
||||
`<button class="btn btn-glass btn-small" disabled style="opacity:0.5">Demnächst</button>`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.innerHTML += card;
|
||||
});
|
||||
} catch (e) {
|
||||
container.innerHTML = "<p>Fehler beim Laden der Download-Liste.</p>";
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Admin Stats (Sends Authorization Bearer token)
|
||||
async function fetchAdminStats() {
|
||||
const sessionStr = localStorage.getItem("veritas_session");
|
||||
if (!sessionStr) return;
|
||||
const session = JSON.parse(sessionStr);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/admin/stats`, {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${session.token}`
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const usersEl = document.getElementById("admin-stat-users");
|
||||
const licsEl = document.getElementById("admin-stat-licenses");
|
||||
const actsEl = document.getElementById("admin-stat-activations");
|
||||
|
||||
if (usersEl) usersEl.textContent = data.total_users;
|
||||
if (licsEl) licsEl.textContent = data.total_licenses;
|
||||
if (actsEl) actsEl.textContent = data.total_activations;
|
||||
|
||||
const tbody = document.getElementById("admin-licenses-table-body");
|
||||
if (tbody) {
|
||||
tbody.innerHTML = "";
|
||||
data.licenses.forEach(lic => {
|
||||
const tr = `
|
||||
<tr style="border-bottom: 1px solid rgba(255,255,255,0.05);">
|
||||
<td style="padding: 12px;">#${lic.id}</td>
|
||||
<td style="padding: 12px;"><code style="color:#38bdf8">${lic.license_key}</code></td>
|
||||
<td style="padding: 12px;">${lic.user_email || 'Gast / System'}</td>
|
||||
<td style="padding: 12px;">${lic.plan_name}</td>
|
||||
<td style="padding: 12px;">${lic.activated_devices} / ${lic.max_devices}</td>
|
||||
<td style="padding: 12px;"><span class="status-badge ${lic.active ? 'badge-active' : 'badge-planned'}">${lic.active ? '🟢 AKTIV' : '🔴 GESPERRT'}</span></td>
|
||||
<td style="padding: 12px;">
|
||||
<button class="btn btn-small ${lic.active ? 'btn-outline-danger' : 'btn-emerald'}" onclick="toggleLicense('${lic.license_key}', ${!lic.active})">
|
||||
${lic.active ? '🚫 Sperren' : '✅ Freischalten'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
tbody.innerHTML += tr;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
showAdminAlert("Zugriff verweigert: " + (data.message || "Admin Privilegien erforderlich"), "error");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Admin stats fetch failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Admin Create License Handler (Sends Authorization Bearer token)
|
||||
async function handleAdminCreateLicense(event) {
|
||||
event.preventDefault();
|
||||
const sessionStr = localStorage.getItem("veritas_session");
|
||||
if (!sessionStr) return;
|
||||
const session = JSON.parse(sessionStr);
|
||||
|
||||
const userId = parseInt(document.getElementById("admin-create-uid").value);
|
||||
const planName = document.getElementById("admin-create-plan").value;
|
||||
const maxDevices = parseInt(document.getElementById("admin-create-max").value);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/admin/create-license`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${session.token}`
|
||||
},
|
||||
body: JSON.stringify({ user_id: userId, plan_name: planName, max_devices: maxDevices })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showAdminAlert("Lizenz erfolgreich erstellt: " + data.message, "success");
|
||||
fetchAdminStats();
|
||||
} else {
|
||||
showAdminAlert("Fehler: " + data.message, "error");
|
||||
}
|
||||
} catch (e) {
|
||||
showAdminAlert("Netzwerkfehler beim Erstellen der Lizenz.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Admin Toggle License Active/Deactive (Sends Authorization Bearer token)
|
||||
async function toggleLicense(licenseKey, newActiveState) {
|
||||
const sessionStr = localStorage.getItem("veritas_session");
|
||||
if (!sessionStr) return;
|
||||
const session = JSON.parse(sessionStr);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/admin/toggle-license`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${session.token}`
|
||||
},
|
||||
body: JSON.stringify({ license_key: licenseKey, active: newActiveState })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showAdminAlert("Lizenz-Status aktualisiert.", "success");
|
||||
fetchAdminStats();
|
||||
} else {
|
||||
showAdminAlert("Fehler: " + data.message, "error");
|
||||
}
|
||||
} catch (e) {
|
||||
showAdminAlert("Netzwerkfehler beim Ändern des Lizenzstatus.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Activation Test
|
||||
async function handleTestActivation(event) {
|
||||
event.preventDefault();
|
||||
const licenseKey = document.getElementById("sim-key").value;
|
||||
const hardwareId = document.getElementById("sim-hwid").value;
|
||||
const appName = document.getElementById("sim-app").value;
|
||||
|
||||
const outputBox = document.getElementById("sim-output-box");
|
||||
const jsonText = document.getElementById("sim-json-text");
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/activate`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
license_key: licenseKey,
|
||||
hardware_id: hardwareId,
|
||||
app_name: appName
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
outputBox.style.display = "block";
|
||||
jsonText.textContent = JSON.stringify(data, null, 2);
|
||||
|
||||
refreshDashboard();
|
||||
} catch (e) {
|
||||
outputBox.style.display = "block";
|
||||
jsonText.textContent = "Fehler bei der Kommunikation mit dem VPS Server.";
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Show Alert for Customer Portal
|
||||
function showAlert(msg, type) {
|
||||
const box = document.getElementById("auth-alert");
|
||||
if (!box) return;
|
||||
box.textContent = msg;
|
||||
box.className = `alert-box ${type}`;
|
||||
box.style.display = "block";
|
||||
setTimeout(() => {
|
||||
box.style.display = "none";
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Helper: Show Alert for Admin Portal
|
||||
function showAdminAlert(msg, type) {
|
||||
const box = document.getElementById("admin-alert");
|
||||
if (!box) return;
|
||||
box.textContent = msg;
|
||||
box.className = `alert-box ${type}`;
|
||||
box.style.display = "block";
|
||||
setTimeout(() => {
|
||||
box.style.display = "none";
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Helper: Copy Key
|
||||
function copyKey(text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert("Lizenzschlüssel in die Zwischenablage kopiert!");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Software Download-Center | Veritas Suite</title>
|
||||
<meta name="description" content="Offizielles Download-Center für die Veritas Suite Executables: Veritas SpoolerReset, Veritas RansomGuard und Module.">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Background Orbs -->
|
||||
<div class="glow-orb orb-purple"></div>
|
||||
<div class="glow-orb orb-cyan"></div>
|
||||
<div class="glow-orb orb-emerald"></div>
|
||||
|
||||
<!-- Header Navigation -->
|
||||
<header class="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="/" class="logo">
|
||||
<div class="logo-shield">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
||||
</div>
|
||||
<div class="logo-brand">
|
||||
<span class="brand-name">VERITAS</span>
|
||||
<span class="brand-tag">SUITE v1.0</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<nav class="nav-links">
|
||||
<a href="/" class="nav-item">🌐 Startseite</a>
|
||||
<a href="/#sparten" class="nav-item">📦 Software-Sparten</a>
|
||||
<a href="/downloads" class="nav-item active">📥 Software-Downloads</a>
|
||||
<a href="/portal" class="nav-item">👤 Kundenportal</a>
|
||||
<a href="https://mail.kanjiv.at/SOGo/index/" target="_blank" class="nav-item" style="color: #38bdf8;">📧 E-Mail Dienst</a>
|
||||
<a href="https://git.kanjiv.at/" target="_blank" class="nav-item" style="color: #a855f7;">🐙 Git Server</a>
|
||||
<a href="/admin" class="nav-item" style="color: #fbbf24;">👑 Admin-Zentrale</a>
|
||||
</nav>
|
||||
|
||||
<div class="server-status-pill" id="status-pill">
|
||||
<span class="status-indicator"></span>
|
||||
<span id="status-text">Server: Verbinde...</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Section -->
|
||||
<section class="section" style="padding-top: 7rem;">
|
||||
<div class="container">
|
||||
<div class="section-title-box">
|
||||
<span class="sub-title">OFFIZIELLES DOWNLOAD-CENTER</span>
|
||||
<h2>Veritas Suite Software-Pakete</h2>
|
||||
<p>Lade fertige, frei stehende Windows Executables (.exe) direkt herunter. Alle Tools sind für 64-Bit Windows optimiert.</p>
|
||||
</div>
|
||||
|
||||
<!-- Active Downloads Grid -->
|
||||
<div id="downloads-container" class="tools-grid">
|
||||
<div class="tool-card glass-panel">
|
||||
<div class="card-top">
|
||||
<span class="sparte-tag tag-cura">CURA FIXER</span>
|
||||
<span class="status-badge badge-active">✅ Bereit zum Download</span>
|
||||
</div>
|
||||
<h3>Veritas SpoolerReset</h3>
|
||||
<p class="tool-desc">Autonomous Windows Druckspooler & Warteschlangen-Reparatur mit GPU GUI, UAC Elevation & VPS Lizenzprüfung.</p>
|
||||
<div class="card-footer">
|
||||
<span class="tech-tag">v1.2.0 Pro</span>
|
||||
<span class="tech-tag">14.2 MB</span>
|
||||
<a href="/spooler-reset.exe" class="btn btn-emerald" style="margin-left: auto;">📥 Download (.exe)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-card glass-panel">
|
||||
<div class="card-top">
|
||||
<span class="sparte-tag tag-aegis">AEGIS SECURITY</span>
|
||||
<span class="status-badge badge-active">✅ Bereit zum Download</span>
|
||||
</div>
|
||||
<h3>Veritas RansomGuard</h3>
|
||||
<p class="tool-desc">Echtzeit Shannon-Entropie Scanner & Kanarienvogel-Köder gegen Krypto-Trojaner mit Autostart & VPS Aktivierung.</p>
|
||||
<div class="card-footer">
|
||||
<span class="tech-tag">v1.0.0 Pro</span>
|
||||
<span class="tech-tag">14.4 MB</span>
|
||||
<a href="/ransomguard.exe" class="btn btn-emerald" style="margin-left: auto;">📥 Download (.exe)</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Requirements Card -->
|
||||
<div class="portal-card glass-panel" style="margin-top: 3rem; padding: 1.5rem; border-radius: 14px;">
|
||||
<h3 style="margin-bottom: 1rem; color: #fff;">🖥️ Systemanforderungen & Installation</h3>
|
||||
<ul style="color: #cbd5e1; line-height: 1.8; margin-left: 1.2rem;">
|
||||
<li><strong>Betriebssystem:</strong> Windows 10, Windows 11 oder Windows Server 2019/2022 (64-Bit).</li>
|
||||
<li><strong>Voraussetzungen:</strong> Keine externen Runtime-DLLs erforderlich (100% Statisch kompiliertes MinGW/GCC Rust Binary).</li>
|
||||
<li><strong>Rechte:</strong> Administrator-Rechte empfohlen (Erfordert UAC Bestätigung beim Start).</li>
|
||||
<li><strong>Lizenzierung:</strong> Nach dem Start einfach mit deinen Kundenkonto-Daten von <a href="/portal" style="color: #38bdf8;">https://kanjiv.at/portal</a> anmelden.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container footer-content">
|
||||
<div class="footer-brand">
|
||||
<span class="logo-shield">🛡️</span>
|
||||
<span>VERITAS SUITE v1.0</span>
|
||||
</div>
|
||||
<p>© 2026 Veritas Suite. Betrieben auf VPS kanjiv.at.</p>
|
||||
<div class="footer-links">
|
||||
<a href="/">Startseite</a>
|
||||
<a href="/downloads">Downloads</a>
|
||||
<a href="/portal">Kundenportal</a>
|
||||
<a href="/admin">Admin-Zentrale</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Veritas Suite v1.0 | Cyber-Security, Forensik & Windows Optimization</title>
|
||||
<meta name="description" content="Veritas Suite v1.0 - Modulare High-Performance Rust Software-Suite für Forensik, B2B-Security und automatisierte Systemreparatur.">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Background Orbs -->
|
||||
<div class="glow-orb orb-purple"></div>
|
||||
<div class="glow-orb orb-cyan"></div>
|
||||
<div class="glow-orb orb-emerald"></div>
|
||||
|
||||
<!-- Header Navigation -->
|
||||
<header class="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="/" class="logo">
|
||||
<div class="logo-shield">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
||||
</div>
|
||||
<div class="logo-brand">
|
||||
<span class="brand-name">VERITAS</span>
|
||||
<span class="brand-tag">SUITE v1.0</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<nav class="nav-links">
|
||||
<a href="/" class="nav-item active">🌐 Startseite</a>
|
||||
<a href="/#sparten" class="nav-item">📦 Software-Sparten</a>
|
||||
<a href="/downloads" class="nav-item">📥 Software-Downloads</a>
|
||||
<a href="/portal" class="nav-item">👤 Kundenportal</a>
|
||||
<a href="https://mail.kanjiv.at/SOGo/index/" target="_blank" class="nav-item" style="color: #38bdf8;">📧 E-Mail Dienst</a>
|
||||
<a href="https://git.kanjiv.at/" target="_blank" class="nav-item" style="color: #a855f7;">🐙 Git Server</a>
|
||||
<a href="/admin" class="nav-item" style="color: #fbbf24;">👑 Admin-Zentrale</a>
|
||||
</nav>
|
||||
|
||||
<div class="server-status-pill" id="status-pill">
|
||||
<span class="status-indicator"></span>
|
||||
<span id="status-text">Server: Verbinde...</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero" id="hero">
|
||||
<div class="h <div class="hero-badge">
|
||||
<span class="badge-sparkle">⚡</span>
|
||||
<span>Fertiggestellte Software & Live Cloud-Backend</span>
|
||||
</div>
|
||||
|
||||
<h1 class="hero-heading">
|
||||
Hochperformante Software für <span class="gradient-text">Security & Windows-Reparatur</span>
|
||||
</h1>
|
||||
|
||||
<p class="hero-description">
|
||||
Die Veritas Suite bietet fertige, kommerzielle High-Performance Tools mit kryptografischem Lizenz- & Cloud-Ökosystem. Entwickelt in sicherem, rasend schnellem Rust.
|
||||
</p>
|
||||
|
||||
<div class="hero-buttons">
|
||||
<a href="/downloads" class="btn btn-glow">
|
||||
<span>Software Downloads</span>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</a>
|
||||
<a href="/portal" class="btn btn-glass">
|
||||
<span>👤 Kundenportal & Lizenzen</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Stats Bar -->
|
||||
<div class="hero-stats glass-panel">
|
||||
<div class="stat-box">
|
||||
<span class="stat-val gradient-emerald">2 FERTIG</span>
|
||||
<span class="stat-desc">Programme bereit</span>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat-box">
|
||||
<span class="stat-val">2</span>
|
||||
<span class="stat-desc">Aktive Sparten</span>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat-box">
|
||||
<span class="stat-val gradient-cyan">100% LIVE</span>
|
||||
<span class="stat-desc">Download-Status</span>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat-box">
|
||||
<span class="stat-val gradient-emerald">VPS ONLINE</span>
|
||||
<span class="stat-desc">Cloud-Backend</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Software Sparten Section -->
|
||||
<section class="section" id="sparten">
|
||||
<div class="container">
|
||||
<div class="section-title-box">
|
||||
<span class="sub-title">FERTIGE SOFTWARE-PAKETE</span>
|
||||
<h2>Verfügbare Programme der Veritas Suite</h2>
|
||||
<p>Übersicht aller einsatzbereiten Werkzeuge zum direkten Download.</p>
|
||||
</div>
|
||||
|
||||
<!-- Category Filter Bar -->
|
||||
<div class="filter-bar">
|
||||
<button class="filter-btn active" onclick="filterSparten('all')">Alle fertigen Tools (2)</button>
|
||||
<button class="filter-btn" onclick="filterSparten('aegis')">🛡️ Sparte AEGIS (Security)</button>
|
||||
<button class="filter-btn" onclick="filterSparten('cura')">🔧 Sparte CURA (System Fixer)</button>
|
||||
</div>
|
||||
|
||||
<!-- Tools Grid -->
|
||||
<div class="tools-grid" id="tools-grid">
|
||||
|
||||
<!-- FERTIGES TOOL 1 -->
|
||||
<div class="tool-card glass-panel" data-sparte="aegis">
|
||||
<div class="card-top">
|
||||
<span class="sparte-tag tag-aegis">AEGIS SECURITY</span>
|
||||
<span class="status-badge badge-active">✅ Bereit zum Download</span>
|
||||
</div>
|
||||
<h3>Veritas RansomGuard</h3>
|
||||
<p class="tool-desc">Echtzeit Shannon-Entropie Scanner im Hintergrund. Stoppt Krypto-Trojaner vor Massenverschlüsselungen & setzt Kanarienvogel-Köder gegen Ransomware.</p>
|
||||
<div class="card-footer">
|
||||
<span class="tech-tag">v1.0.0 Pro</span>
|
||||
<span class="tech-tag">Anti-Ransomware</span>
|
||||
<a href="/downloads" class="btn btn-emerald" style="margin-left: auto;">Download (.exe)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FERTIGES TOOL 2 -->
|
||||
<div class="tool-card glass-panel" data-sparte="cura">
|
||||
<div class="card-top">
|
||||
<span class="sparte-tag tag-cura">CURA FIXER</span>
|
||||
<span class="status-badge badge-active">✅ Bereit zum Download</span>
|
||||
</div>
|
||||
<h3>Veritas SpoolerReset</h3>
|
||||
<p class="tool-desc">Autonomous Windows Druckspooler & Warteschlangen-Reparatur mit GPU GUI, automatischem UAC Elevation & VPS Lizenzprüfung.</p>
|
||||
<div class="card-footer">
|
||||
<span class="tech-tag">v1.2.0 Pro</span>
|
||||
<span class="tech-tag">Spooler Fix</span>
|
||||
<a href="/downloads" class="btn btn-emerald" style="margin-left: auto;">Download (.exe)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>he</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container footer-content">
|
||||
<div class="footer-brand">
|
||||
<span class="logo-shield">🛡️</span>
|
||||
<span>VERITAS SUITE v1.0</span>
|
||||
</div>
|
||||
<p>© 2026 Veritas Suite. Betrieben auf VPS kanjiv.at.</p>
|
||||
<div class="footer-links">
|
||||
<a href="/">Startseite</a>
|
||||
<a href="/downloads">Downloads</a>
|
||||
<a href="/portal">Kundenportal</a>
|
||||
<a href="/admin">Admin-Zentrale</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,223 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kundenportal & Control Center | Veritas Suite</title>
|
||||
<meta name="description" content="Alles auf einen Blick: Veritas Suite Kundenportal für Lizenzen, Downloads, Account-Einstellungen und Live-Aktivierung.">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Background Orbs -->
|
||||
<div class="glow-orb orb-purple"></div>
|
||||
<div class="glow-orb orb-cyan"></div>
|
||||
<div class="glow-orb orb-emerald"></div>
|
||||
|
||||
<!-- Header Navigation -->
|
||||
<header class="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="/" class="logo">
|
||||
<div class="logo-shield">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
||||
</div>
|
||||
<div class="logo-brand">
|
||||
<span class="brand-name">VERITAS</span>
|
||||
<span class="brand-tag">SUITE v1.0</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<nav class="nav-links">
|
||||
<a href="/" class="nav-item">🌐 Startseite</a>
|
||||
<a href="/#sparten" class="nav-item">📦 Software-Sparten</a>
|
||||
<a href="/downloads" class="nav-item">📥 Software-Downloads</a>
|
||||
<a href="/portal" class="nav-item active portal-link">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
|
||||
Kundenportal
|
||||
</a>
|
||||
<a href="https://mail.kanjiv.at/SOGo/index/" target="_blank" class="nav-item" style="color: #38bdf8;">📧 E-Mail Dienst</a>
|
||||
<a href="https://git.kanjiv.at/" target="_blank" class="nav-item" style="color: #a855f7;">🐙 Git Server</a>
|
||||
<!-- Admin link dynamically visible ONLY for logged-in admins -->
|
||||
<a href="/admin" id="nav-admin-link" class="nav-item" style="display: none; color: #fbbf24; font-weight: 700;">👑 Admin-Zentrale</a>
|
||||
</nav>
|
||||
|
||||
<div class="server-status-pill" id="status-pill">
|
||||
<span class="status-indicator"></span>
|
||||
<span id="status-text">Server: Verbinde...</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Portal Main Single-Page Section -->
|
||||
<section class="section" style="padding-top: 7rem; padding-bottom: 4rem;">
|
||||
<div class="container">
|
||||
|
||||
<!-- Hero Title Box -->
|
||||
<div class="section-title-box" style="margin-bottom: 2rem;">
|
||||
<span class="sub-title">ALL-IN-ONE CONTROL CENTER</span>
|
||||
<h2>Veritas Kundenportal</h2>
|
||||
<p>Verwalte deine Lizenzen, deine Anmeldedaten und deine Software-Downloads auf einer zentralen Übersichtsseite.</p>
|
||||
</div>
|
||||
|
||||
<!-- Global Alert Box -->
|
||||
<div id="auth-alert" class="alert-box" style="display: none; margin-bottom: 2rem; padding: 14px 18px; border-radius: 10px; font-weight: 600;"></div>
|
||||
|
||||
<!-- SECTION 1: ACCOUNT & ANMELDUNG -->
|
||||
<div class="portal-card glass-panel" style="padding: 2rem; border-radius: 16px; border: 1px solid rgba(255,255,255,0.1); margin-bottom: 2.5rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<span style="font-size: 2rem;">👤</span>
|
||||
<div>
|
||||
<h3 style="margin: 0; color: #fff; font-size: 1.3rem;">Konto & Authentifizierung</h3>
|
||||
<p style="margin: 0; color: #94a3b8; font-size: 0.85rem;">Melde dich an oder erstelle ein neues Konto mit automatischer Pro-Lizenz.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logged In Status Bar & Admin Button -->
|
||||
<div id="user-status-bar" style="display: none; background: rgba(16, 185, 129, 0.15); border: 1px solid rgba(16, 185, 129, 0.3); padding: 8px 16px; border-radius: 8px; align-items: center; gap: 12px; flex-wrap: wrap;">
|
||||
<span style="color: #34d399; font-weight: 600; font-size: 0.9rem;" id="dash-user-email">user@domain.de</span>
|
||||
<a href="/admin" id="btn-admin-panel" style="display: none; background: linear-gradient(135deg, #f59e0b, #b45309); color: #fff; font-weight: 700; padding: 6px 14px; border-radius: 6px; text-decoration: none; font-size: 0.85rem;">👑 Admin-Zentrale Öffnen</a>
|
||||
<button class="btn btn-outline-danger btn-small" onclick="logout()">Abmelden</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Forms Grid (Shows when logged out) -->
|
||||
<div id="auth-forms-container" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem;">
|
||||
<!-- Login Box -->
|
||||
<div class="auth-card glass-panel" style="padding: 1.6rem; border-radius: 12px; background: rgba(24, 34, 53, 0.6); border: 1px solid rgba(255,255,255,0.08);">
|
||||
<h4 style="margin-top: 0; margin-bottom: 0.5rem; color: #fff;">🔑 Anmelden</h4>
|
||||
<p style="color: #94a3b8; font-size: 0.85rem; margin-bottom: 1.2rem;">Zugang zu deinen lizenzierten Tools</p>
|
||||
<form onsubmit="handleLogin(event)">
|
||||
<div class="form-group" style="margin-bottom: 1.2rem;">
|
||||
<label style="display: block; margin-bottom: 6px; color: #cbd5e1; font-size: 0.85rem;">E-Mail-Adresse</label>
|
||||
<input type="email" id="login-email" placeholder="name@domain.de" required style="width: 100%; padding: 11px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(3, 7, 18, 0.8); color: #fff;">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||
<label style="display: block; margin-bottom: 6px; color: #cbd5e1; font-size: 0.85rem;">Passwort</label>
|
||||
<input type="password" id="login-password" placeholder="••••••••" required style="width: 100%; padding: 11px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(3, 7, 18, 0.8); color: #fff;">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-glow btn-block" style="width: 100%;">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Register Box -->
|
||||
<div class="auth-card glass-panel" style="padding: 1.6rem; border-radius: 12px; background: rgba(16, 185, 129, 0.06); border: 1px solid rgba(16, 185, 129, 0.25);">
|
||||
<h4 style="margin-top: 0; margin-bottom: 0.5rem; color: #fff;">⚡ Registrieren</h4>
|
||||
<p style="color: #a7f3d0; font-size: 0.85rem; margin-bottom: 1.2rem;">Erhalte sofort eine <strong>Veritas Pro Lizenz</strong> (5 Geräte)</p>
|
||||
<form onsubmit="handleRegister(event)">
|
||||
<div class="form-group" style="margin-bottom: 1.2rem;">
|
||||
<label style="display: block; margin-bottom: 6px; color: #cbd5e1; font-size: 0.85rem;">E-Mail-Adresse</label>
|
||||
<input type="email" id="reg-email" placeholder="neuer.nutzer@domain.de" required style="width: 100%; padding: 11px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(3, 7, 18, 0.8); color: #fff;">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||
<label style="display: block; margin-bottom: 6px; color: #cbd5e1; font-size: 0.85rem;">Passwort festlegen</label>
|
||||
<input type="password" id="reg-password" placeholder="••••••••" required style="width: 100%; padding: 11px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(3, 7, 18, 0.8); color: #fff;">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-emerald btn-block" style="width: 100%;">Konto Erstellen & Pro-Lizenz Erhalten</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 2: LIZENZ-DASHBOARD -->
|
||||
<div class="portal-card glass-panel" style="padding: 2rem; border-radius: 16px; border: 1px solid rgba(255,255,255,0.1); margin-bottom: 2.5rem;">
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 1.5rem;">
|
||||
<span style="font-size: 2rem;">🔑</span>
|
||||
<div>
|
||||
<h3 style="margin: 0; color: #fff; font-size: 1.3rem;">Deine Software-Lizenzen</h3>
|
||||
<p style="margin: 0; color: #94a3b8; font-size: 0.85rem;">Aktive Lizenzen, Schlüssel und verknüpfte Geräte-Slots</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dash-logged-out" class="logged-out-state" style="text-align: center; padding: 2rem 1rem; background: rgba(3, 7, 18, 0.4); border-radius: 12px;">
|
||||
<div style="font-size: 2.5rem; margin-bottom: 0.5rem;">🔒</div>
|
||||
<p style="color: #cbd5e1; margin-bottom: 0;">Bitte melde dich oben an, um deine Lizenzen einzusehen.</p>
|
||||
</div>
|
||||
|
||||
<div id="dash-logged-in" style="display: none;">
|
||||
<div id="license-list" class="license-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 3: SCHNELL-DOWNLOAD CENTER -->
|
||||
<div class="portal-card glass-panel" style="padding: 2rem; border-radius: 16px; border: 1px solid rgba(255,255,255,0.1); margin-bottom: 2.5rem;">
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 1.5rem;">
|
||||
<span style="font-size: 2rem;">📥</span>
|
||||
<div>
|
||||
<h3 style="margin: 0; color: #fff; font-size: 1.3rem;">Software Download-Center</h3>
|
||||
<p style="margin: 0; color: #94a3b8; font-size: 0.85rem;">Lade die fertigen Windows Executables (.exe) direkt herunter</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="downloads-container" class="tools-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 4: INTERAKTIVER AKTIVIERUNGS-TESTER -->
|
||||
<div class="portal-card glass-panel" style="padding: 2rem; border-radius: 16px; border: 1px solid rgba(255,255,255,0.1);">
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 1.5rem;">
|
||||
<span style="font-size: 2rem;">⚡</span>
|
||||
<div>
|
||||
<h3 style="margin: 0; color: #fff; font-size: 1.3rem;">Online Aktivierungs-Tester</h3>
|
||||
<p style="margin: 0; color: #94a3b8; font-size: 0.85rem;">Testet den Handshake zwischen den Windows-Tools und dem VPS Server</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onsubmit="handleTestActivation(event)" class="sim-form">
|
||||
<div class="form-row" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.2rem; margin-bottom: 1.2rem;">
|
||||
<div class="form-group">
|
||||
<label style="display: block; margin-bottom: 6px; color: #cbd5e1; font-size: 0.85rem;">Lizenzschlüssel</label>
|
||||
<input type="text" id="sim-key" placeholder="VERITAS-PRO-XXXXXX" required style="width: 100%; padding: 11px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(3, 7, 18, 0.8); color: #38bdf8; font-family: monospace;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="display: block; margin-bottom: 6px; color: #cbd5e1; font-size: 0.85rem;">Simulierte Hardware-ID (HWID)</label>
|
||||
<input type="text" id="sim-hwid" value="WIN-CPU-CORE-i9-99X" required style="width: 100%; padding: 11px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(3, 7, 18, 0.8); color: #fff; font-family: monospace;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||
<label style="display: block; margin-bottom: 6px; color: #cbd5e1; font-size: 0.85rem;">Ziel-Applikation</label>
|
||||
<select id="sim-app" style="width: 100%; padding: 11px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(3, 7, 18, 0.8); color: #fff;">
|
||||
<option value="Veritas SpoolerReset Commercial GUI v1.2.0">Veritas SpoolerReset (v1.2.0)</option>
|
||||
<option value="Veritas RansomGuard Commercial GUI v1.0.0">Veritas RansomGuard (v1.0.0)</option>
|
||||
<option value="Veritas Recovery Engine (VRE)">Veritas Recovery Engine (VRE)</option>
|
||||
<option value="Veritas Imager (VIM)">Veritas Imager (VIM)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-glow">Online Aktivierungs-Call Ausführen</button>
|
||||
</form>
|
||||
|
||||
<div id="sim-output-box" class="sim-output glass-panel" style="display: none; margin-top: 1.5rem; padding: 1.2rem; border-radius: 10px; background: rgba(3, 7, 18, 0.9); border: 1px solid rgba(99, 102, 241, 0.4);">
|
||||
<div class="output-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<span style="color: #94a3b8; font-size: 0.85rem; font-family: monospace;">SERVER RESPONSE JSON</span>
|
||||
<span class="badge-status-code" style="background: rgba(16, 185, 129, 0.2); color: #34d399; padding: 4px 8px; border-radius: 4px; font-size: 0.8rem;">HTTP 200 OK</span>
|
||||
</div>
|
||||
<pre id="sim-json-text" style="color: #6ee7b7; font-family: monospace; font-size: 0.95rem; margin: 0; white-space: pre-wrap;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container footer-content">
|
||||
<div class="footer-brand">
|
||||
<span class="logo-shield">🛡️</span>
|
||||
<span>VERITAS SUITE v1.0</span>
|
||||
</div>
|
||||
<p>© 2026 Veritas Suite. Betrieben auf VPS kanjiv.at.</p>
|
||||
<div class="footer-links">
|
||||
<a href="/">Startseite</a>
|
||||
<a href="/downloads">Downloads</a>
|
||||
<a href="/portal">Kundenportal</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,598 @@
|
||||
/* ==========================================================================
|
||||
VERITAS SUITE v1.0 - PREMIUM DESIGN SYSTEM (Obsidian & Cyber Glass)
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
--bg-main: #030712;
|
||||
--bg-surface: rgba(15, 23, 42, 0.75);
|
||||
--bg-panel: rgba(30, 41, 59, 0.55);
|
||||
--border-glass: rgba(255, 255, 255, 0.12);
|
||||
--border-light: rgba(255, 255, 255, 0.22);
|
||||
|
||||
--indigo: #6366f1;
|
||||
--indigo-glow: rgba(99, 102, 241, 0.45);
|
||||
--cyan: #38bdf8;
|
||||
--cyan-glow: rgba(56, 189, 248, 0.45);
|
||||
--emerald: #10b981;
|
||||
--emerald-glow: rgba(16, 185, 129, 0.45);
|
||||
--amber: #f59e0b;
|
||||
--amber-glow: rgba(245, 158, 11, 0.45);
|
||||
--rose: #f43f5e;
|
||||
|
||||
--text-heading: #ffffff;
|
||||
--text-body: #cbd5e1;
|
||||
--text-muted: #94a3b8;
|
||||
--text-dim: #64748b;
|
||||
|
||||
--font-heading: 'Outfit', sans-serif;
|
||||
--font-body: 'Inter', sans-serif;
|
||||
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 16px;
|
||||
--radius-lg: 24px;
|
||||
--transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-main);
|
||||
color: var(--text-body);
|
||||
font-family: var(--font-body);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
/* Background Glow Orbs */
|
||||
.glow-orb {
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
filter: blur(150px);
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.orb-purple {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(circle, var(--indigo) 0%, transparent 70%);
|
||||
top: -200px;
|
||||
left: -150px;
|
||||
}
|
||||
.orb-cyan {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: radial-gradient(circle, var(--cyan) 0%, transparent 70%);
|
||||
top: 30%;
|
||||
right: -200px;
|
||||
}
|
||||
.orb-emerald {
|
||||
width: 550px;
|
||||
height: 550px;
|
||||
background: radial-gradient(circle, var(--emerald) 0%, transparent 70%);
|
||||
bottom: -200px;
|
||||
left: 20%;
|
||||
}
|
||||
|
||||
/* Glass Panel Component */
|
||||
.glass-panel {
|
||||
background: var(--bg-surface);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.6);
|
||||
transition: var(--transition);
|
||||
}
|
||||
.glass-panel:hover {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7), 0 0 20px rgba(99, 102, 241, 0.15);
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
max-width: 1240px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
.navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: rgba(3, 7, 18, 0.88);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--border-glass);
|
||||
}
|
||||
.nav-container {
|
||||
max-width: 1240px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
.logo-shield {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: linear-gradient(135deg, var(--indigo) 0%, #38bdf8 100%);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
box-shadow: 0 0 25px var(--indigo-glow);
|
||||
}
|
||||
.logo-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.brand-name {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 1.3rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
color: white;
|
||||
}
|
||||
.brand-tag {
|
||||
font-size: 0.7rem;
|
||||
color: var(--cyan);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
.nav-item {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.nav-item:hover, .nav-item.active {
|
||||
color: white;
|
||||
}
|
||||
.portal-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.55rem 1.2rem;
|
||||
background: rgba(99, 102, 241, 0.18);
|
||||
border: 1px solid rgba(99, 102, 241, 0.4);
|
||||
border-radius: var(--radius-sm);
|
||||
color: #a5b4fc;
|
||||
}
|
||||
.portal-link:hover {
|
||||
background: var(--indigo);
|
||||
color: white;
|
||||
box-shadow: 0 0 25px var(--indigo-glow);
|
||||
}
|
||||
|
||||
.server-status-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.4rem 0.9rem;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: 50px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--amber);
|
||||
box-shadow: 0 0 10px var(--amber-glow);
|
||||
}
|
||||
.status-indicator.online {
|
||||
background: var(--emerald);
|
||||
box-shadow: 0 0 14px var(--emerald-glow);
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero {
|
||||
padding: 6rem 1.5rem 4rem 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
.hero-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 1.3rem;
|
||||
background: rgba(56, 189, 248, 0.12);
|
||||
border: 1px solid rgba(56, 189, 248, 0.35);
|
||||
border-radius: 50px;
|
||||
color: #38bdf8;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-heading {
|
||||
font-size: 3.6rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #a5b4fc 0%, #38bdf8 50%, #34d399 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.hero-description {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-muted);
|
||||
max-width: 720px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
margin-bottom: 3.5rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.85rem 1.8rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.btn-glow {
|
||||
background: linear-gradient(135deg, var(--indigo) 0%, #4f46e5 100%);
|
||||
color: white;
|
||||
box-shadow: 0 0 25px var(--indigo-glow);
|
||||
}
|
||||
.btn-glow:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0 35px var(--indigo-glow);
|
||||
}
|
||||
.btn-glass {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--border-glass);
|
||||
color: var(--text-heading);
|
||||
}
|
||||
.btn-glass:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.btn-emerald {
|
||||
background: linear-gradient(135deg, var(--emerald) 0%, #059669 100%);
|
||||
color: white;
|
||||
box-shadow: 0 0 25px var(--emerald-glow);
|
||||
}
|
||||
.btn-emerald:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0 35px var(--emerald-glow);
|
||||
}
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
.btn-outline-danger {
|
||||
background: rgba(244, 63, 94, 0.12);
|
||||
border: 1px solid rgba(244, 63, 94, 0.4);
|
||||
color: #fda4af;
|
||||
padding: 0.45rem 1.1rem;
|
||||
font-size: 0.85rem;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-outline-danger:hover {
|
||||
background: var(--rose);
|
||||
color: white;
|
||||
}
|
||||
.btn-small {
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Hero Stats Bar */
|
||||
.hero-stats {
|
||||
width: 100%;
|
||||
padding: 1.8rem 2.5rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
.stat-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.stat-val {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
}
|
||||
.gradient-cyan {
|
||||
background: linear-gradient(135deg, var(--cyan), #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.gradient-emerald {
|
||||
background: linear-gradient(135deg, var(--emerald), #34d399);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.stat-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.stat-sep {
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background: var(--border-glass);
|
||||
}
|
||||
|
||||
/* Section Title Box */
|
||||
.section {
|
||||
padding: 5rem 0;
|
||||
}
|
||||
.section-title-box {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.sub-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--cyan);
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
.section-title-box h2 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.section-title-box p {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Sparten Filter Bar */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
margin-bottom: 3rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filter-btn {
|
||||
padding: 0.65rem 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: 50px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.filter-btn:hover {
|
||||
color: white;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.filter-btn.active {
|
||||
background: var(--indigo);
|
||||
color: white;
|
||||
border-color: var(--indigo);
|
||||
box-shadow: 0 0 25px var(--indigo-glow);
|
||||
}
|
||||
|
||||
/* Tools Grid */
|
||||
.tools-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 1.8rem;
|
||||
}
|
||||
.tool-card {
|
||||
padding: 1.8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tool-card:hover {
|
||||
transform: translateY(-6px);
|
||||
border-color: var(--border-light);
|
||||
box-shadow: 0 20px 40px -10px rgba(99, 102, 241, 0.25);
|
||||
}
|
||||
.card-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.sparte-tag {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tag-veritas {
|
||||
background: rgba(99, 102, 241, 0.18);
|
||||
color: #a5b4fc;
|
||||
border: 1px solid rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
.tag-aegis {
|
||||
background: rgba(244, 63, 94, 0.18);
|
||||
color: #fda4af;
|
||||
border: 1px solid rgba(244, 63, 94, 0.4);
|
||||
}
|
||||
.tag-cura {
|
||||
background: rgba(16, 185, 129, 0.18);
|
||||
color: #6ee7b7;
|
||||
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-radius: 50px;
|
||||
}
|
||||
.badge-active {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #34d399;
|
||||
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
.badge-dev {
|
||||
background: rgba(56, 189, 248, 0.2);
|
||||
color: #38bdf8;
|
||||
border: 1px solid rgba(56, 189, 248, 0.4);
|
||||
}
|
||||
.badge-planned {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #fde047;
|
||||
border: 1px solid rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
|
||||
.tool-card h3 {
|
||||
font-size: 1.35rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.tool-desc {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.92rem;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.card-footer {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
.tech-tag {
|
||||
font-size: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--border-glass);
|
||||
padding: 0.25rem 0.7rem;
|
||||
border-radius: 4px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* Form Controls */
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.form-group label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.form-group input, .form-group select {
|
||||
padding: 0.8rem 1rem;
|
||||
background: #090d16;
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: var(--radius-sm);
|
||||
color: white;
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.form-group input:focus, .form-group select:focus {
|
||||
border-color: var(--indigo);
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.alert-box {
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: var(--radius-sm);
|
||||
display: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.alert-box.success {
|
||||
display: block;
|
||||
background: rgba(16, 185, 129, 0.18);
|
||||
border: 1px solid rgba(16, 185, 129, 0.5);
|
||||
color: #6ee7b7;
|
||||
}
|
||||
.alert-box.error {
|
||||
display: block;
|
||||
background: rgba(244, 63, 94, 0.18);
|
||||
border: 1px solid rgba(244, 63, 94, 0.5);
|
||||
color: #fda4af;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
border-top: 1px solid var(--border-glass);
|
||||
padding: 2.5rem 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.footer-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.footer-links a {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.footer-links a:hover {
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
sudo -u postgres psql -d veritas_db -c "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);"
|
||||
sudo -u postgres psql -d veritas_db -c "ALTER TABLE licenses ADD COLUMN IF NOT EXISTS user_id INT REFERENCES users(id);"
|
||||
sudo -u postgres psql -d veritas_db -c "ALTER TABLE licenses ADD COLUMN IF NOT EXISTS plan_name VARCHAR(64) DEFAULT 'Pro';"
|
||||
sudo -u postgres psql -d veritas_db -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO veritas;"
|
||||
sudo -u postgres psql -d veritas_db -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO veritas;"
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
sudo -u postgres psql -d veritas_db -c "ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN DEFAULT FALSE;"
|
||||
sudo -u postgres psql -d veritas_db -c "UPDATE users SET is_admin = TRUE WHERE id = 1 OR email = 'admin@kanjiv.at';"
|
||||
sudo -u postgres psql -d veritas_db -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO veritas;"
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
sudo -u postgres psql -d veritas_db -c "CREATE TABLE IF NOT EXISTS licenses (id SERIAL PRIMARY KEY, license_key VARCHAR(64) UNIQUE NOT NULL, max_devices INT DEFAULT 3, active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);"
|
||||
sudo -u postgres psql -d veritas_db -c "CREATE TABLE IF NOT EXISTS activations (id SERIAL PRIMARY KEY, license_key VARCHAR(64) NOT NULL, hardware_id VARCHAR(128) NOT NULL, app_name VARCHAR(64) NOT NULL, activated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT unique_activation UNIQUE(license_key, hardware_id));"
|
||||
sudo -u postgres psql -d veritas_db -c "INSERT INTO licenses (license_key, max_devices) VALUES ('VERITAS-PRO-DEMO-2026', 10) ON CONFLICT DO NOTHING;"
|
||||
@@ -0,0 +1,6 @@
|
||||
vps IP 45.156.84.238
|
||||
vps benutzer : root
|
||||
vps PW: Matthias2710..
|
||||
|
||||
---------------------------------
|
||||
|
||||
Reference in New Issue
Block a user