feat(magic-win): ReBreak Magic Windows-App (Tauri) + CI-Build
Tauri 2 DoH-Schutz für Windows: PowerShell-DoH-Takeover, Tamper-Service (SYSTEM, windows-service), Browser-Policies (Chromium built-in-DNS + eigenes DoH aus → OS-Resolver), 24h-Cooldown via bestehende magic/*-Endpoints. GitHub-Actions baut den x64-NSIS-Installer auf windows-latest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
61
.github/workflows/build-magic-win.yml
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
name: Build Magic Windows
|
||||
|
||||
# Baut den NSIS-Installer der ReBreak-Magic-Windows-App (Tauri) auf einem
|
||||
# echten Windows-Runner — vom Mac aus geht kein Cross-Compile (MSVC + WebView2).
|
||||
# Artefakt: x64-Installer, herunterladbar unter Actions → Run → Artifacts.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "apps/rebreak-magic-win/**"
|
||||
- ".github/workflows/build-magic-win.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: build-magic-win
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: NSIS Installer (x64)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install deps (nur Magic-Win)
|
||||
run: pnpm install --filter rebreak-magic-win --no-frozen-lockfile
|
||||
|
||||
- name: Build Tamper-Service (Tauri-Sidecar)
|
||||
working-directory: apps/rebreak-magic-win/src-tauri
|
||||
shell: pwsh
|
||||
run: |
|
||||
cargo build --release -p rebreak-protection-service
|
||||
$triple = (rustc -vV | Select-String 'host: (.*)').Matches.Groups[1].Value
|
||||
New-Item -ItemType Directory -Force -Path binaries | Out-Null
|
||||
Copy-Item "target/release/rebreak-protection-service.exe" "binaries/rebreak-protection-service-$triple.exe" -Force
|
||||
|
||||
- name: Build Tauri-App (Frontend + NSIS)
|
||||
working-directory: apps/rebreak-magic-win
|
||||
run: pnpm tauri build
|
||||
|
||||
- name: Upload Installer
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: RebreakMagic-Windows-x64
|
||||
path: apps/rebreak-magic-win/src-tauri/target/release/bundle/nsis/*.exe
|
||||
if-no-files-found: error
|
||||
5
apps/rebreak-magic-win/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
src-tauri/target/
|
||||
src-tauri/binaries/rebreak-protection-service-*
|
||||
config.json
|
||||
111
apps/rebreak-magic-win/README.md
Normal file
@ -0,0 +1,111 @@
|
||||
# ReBreak Magic für Windows
|
||||
|
||||
Windows-Companion-App für ReBreak — DNS-basierter Glücksspiel-Schutz mit
|
||||
24h-Cooldown und Tamper-Protection. Das Windows-Pendant zu
|
||||
`apps/rebreak-magic-mac` (gleiches Backend, gleiche `/api/magic/*`-Endpoints,
|
||||
gleicher Look: Ketten-Icon, Nunito, Accent `#2E7FD4`).
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
ReBreak Magic (Tauri 2)
|
||||
├── src/ React-Frontend (Nunito, ReBreak-Look)
|
||||
│ ├── views/LoginView 6-stelliger Pairing-Code (wie Mac-App)
|
||||
│ └── views/HubView Schutzstatus, Aktivierung, 24h-Countdown
|
||||
├── src-tauri/src/ Rust-Core — Tokens erreichen die WebView NIE
|
||||
│ ├── api.rs Backend-Client (/api/magic/*)
|
||||
│ ├── auth.rs Windows Credential Manager (keyring)
|
||||
│ ├── device.rs Device-ID via MachineGuid
|
||||
│ ├── setup.rs Elevated One-Shot (EIN UAC-Prompt)
|
||||
│ └── lib.rs Tauri-Commands + UI-State
|
||||
├── src-tauri/protection-core/ Geteilte DoH-Logik (App + Service)
|
||||
└── src-tauri/service/ Tamper-Service (Windows-Service, SYSTEM)
|
||||
```
|
||||
|
||||
### Flow
|
||||
|
||||
1. **Koppeln** — User erstellt in der ReBreak-App einen 6-stelligen Code
|
||||
(`POST /api/magic/pair/redeem` → `mgc_`-Session-Token → Credential Manager).
|
||||
Direkt danach `POST /api/magic/register` (`platform: "windows"`) →
|
||||
per-Device-DNS-Token, AdGuard-Client wird serverseitig provisioniert.
|
||||
2. **Schutz aktivieren** — EIN UAC-Prompt führt ein One-Shot-PowerShell aus:
|
||||
- Windows-DoH auf `https://dns.rebreak.org/dns-query/<token>` konfigurieren
|
||||
(`Add-DnsClientDohServerAddress`, Interface-DNS, `EnableAutoDoh=2`,
|
||||
per-Interface `DohFlags=1`, flushdns)
|
||||
- State nach `%ProgramData%\ReBreak\protection.json`
|
||||
- Tamper-Service installieren + starten (`sc.exe`, `obj= LocalSystem`,
|
||||
Auto-Start, Auto-Restart bei Crash)
|
||||
3. **Tamper-Protection** — Service prüft alle 5 Minuten:
|
||||
- DoH manipuliert → sofort wiederherstellen
|
||||
- Backend offline → **fail-closed**, Schutz bleibt
|
||||
4. **Deaktivierung (24h-Cooldown)** — `request-release` startet den Cooldown,
|
||||
die App zeigt den Countdown. Nach 24h revoked der Backend-Cron den Token;
|
||||
der Service sieht `active=false` via `GET /api/magic/status?token=` und
|
||||
baut den DoH-Schutz **selbst** ab (SYSTEM, kein zweiter UAC-Prompt).
|
||||
Die unelevated App kann den Schutz nicht abbauen — Deaktivierung ist
|
||||
ausschließlich server-zeitgesteuert, es gibt keinen Code, den der User
|
||||
sehen oder eingeben könnte.
|
||||
|
||||
### Bewusste Abweichungen vom ursprünglichen Prompt-Dokument
|
||||
|
||||
| Prompt | Umgesetzt | Warum |
|
||||
|---|---|---|
|
||||
| Electron + node-windows | **Tauri 2 + Rust-Service** | ~5 MB statt ~150 MB, sauberer SYSTEM-Service, kein Node auf dem Zielsystem |
|
||||
| NextDNS pro User | **Eigene DoH-Infra** (`dns.rebreak.org`, AdGuard) | Existiert bereits inkl. Provisioning in `magic/register`; kein neuer DSGVO-Sub-Auftragsverarbeiter, keine Kosten pro User, kuratierte Liste statt generischer Gambling-Kategorie |
|
||||
| Supabase Edge Functions + eigene Tabelle | **Bestehendes Nitro-Backend** | `request-release`/`cancel-release` + Cron-Invalidierung existieren bereits (Mac-App nutzt denselben Flow) |
|
||||
| Code verschlüsselt in DB, App empfängt Code | **Kein Code nötig** | Deaktivierung ist server-zeitgesteuert: Token-Revoke nach 24h, Service pollt Status |
|
||||
|
||||
### Backend-Änderungen (in diesem Repo, additiv)
|
||||
|
||||
- `backend/server/api/magic/register.post.ts` — optionales `platform`-Feld
|
||||
(`"windows"`, Default bleibt `"macos"` für die Mac-App)
|
||||
- `backend/server/api/magic/status.get.ts` — **neu**: Token-Status-Polling
|
||||
für den Tamper-Service (`{ active: boolean }`, Token ist das Secret)
|
||||
|
||||
## Entwicklung (macOS)
|
||||
|
||||
```sh
|
||||
pnpm install # Repo-Root
|
||||
cd apps/rebreak-magic-win
|
||||
pnpm build # tsc + vite
|
||||
cd src-tauri
|
||||
cargo build -p rebreak-protection-service
|
||||
cp target/debug/rebreak-protection-service "binaries/rebreak-protection-service-$(rustc -vV | sed -n 's/host: //p')"
|
||||
cargo check --workspace && cargo test --workspace
|
||||
```
|
||||
|
||||
PowerShell-Ausführung ist `#[cfg(windows)]`-frei gehalten (Commands sind
|
||||
Strings) — auf macOS schlagen die `powershell.exe`-Aufrufe einfach fehl und
|
||||
der Status ist `MISSING`. UI-Entwicklung geht mit `pnpm tauri dev` auch am Mac.
|
||||
|
||||
## Build (Windows-Maschine)
|
||||
|
||||
```powershell
|
||||
pnpm install # Repo-Root
|
||||
cd apps\rebreak-magic-win
|
||||
.\build-windows.ps1 # Service + App + NSIS-Installer (perMachine)
|
||||
```
|
||||
|
||||
Service-Debug ohne SCM: `rebreak-protection-service.exe --console`
|
||||
(Log: `%ProgramData%\ReBreak\service.log`).
|
||||
|
||||
Backend-Override (analog `config.example.json` der Mac-App):
|
||||
`%APPDATA%\org.rebreak.magic\config.json` → `{ "backendBaseUrl": "https://staging.rebreak.org" }`.
|
||||
Default ist Staging.
|
||||
|
||||
## Bekannte Limitationen (v1)
|
||||
|
||||
- **Browser-eigenes DoH** (Chrome/Firefox „Secure DNS") kann Windows-DoH
|
||||
umgehen → Phase 2 (Browser-Policies via Registry: `DnsOverHttpsMode=off`
|
||||
ist als HKLM-Policy für Chrome/Edge/Firefox setzbar — im selben elevated
|
||||
One-Shot nachrüstbar).
|
||||
- **IPv6-DNS** wird nicht angefasst (IPv4-DoH only, wie im Prompt).
|
||||
Router-advertised IPv6-Resolver sind ein möglicher Bypass → Phase 2.
|
||||
- **Lokale Windows-Admins** können den Service stoppen (`sc delete` etc.).
|
||||
Wie überall bei ReBreak gilt: Friction statt absoluter Unumgehbarkeit —
|
||||
die Hürde ist bewusst hoch (SYSTEM-Service, Auto-Restart, Re-Apply), aber
|
||||
ein entschlossener Admin ist nicht aufhaltbar (gleiche Asymmetrie wie
|
||||
Profil-Entfernung auf unsupervised iOS).
|
||||
- `sc.exe stop/delete` bei der NSIS-Deinstallation ist noch nicht verdrahtet
|
||||
(NSIS-Hook) — der Service deaktiviert sich aber selbst, sobald das Backend
|
||||
den Token revoked.
|
||||
27
apps/rebreak-magic-win/build-windows.ps1
Normal file
@ -0,0 +1,27 @@
|
||||
# ReBreak Magic für Windows — Build-Script (auf einer Windows-Maschine ausführen)
|
||||
#
|
||||
# Voraussetzungen:
|
||||
# - Rust (rustup, MSVC toolchain)
|
||||
# - Node + pnpm (Repo-Root: pnpm install)
|
||||
# - WebView2 Runtime (Win11: vorinstalliert)
|
||||
#
|
||||
# Ergebnis: NSIS-Installer unter src-tauri\target\release\bundle\nsis\
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-Location $PSScriptRoot
|
||||
|
||||
# 1. Tamper-Service als Release bauen und als Tauri-Sidecar ablegen
|
||||
# (externalBin erwartet Suffix mit Target-Triple)
|
||||
cargo build --release -p rebreak-protection-service --manifest-path src-tauri/Cargo.toml
|
||||
|
||||
$triple = (rustc -vV | Select-String "host: (.*)").Matches.Groups[1].Value
|
||||
Copy-Item `
|
||||
"src-tauri/target/release/rebreak-protection-service.exe" `
|
||||
"src-tauri/binaries/rebreak-protection-service-$triple.exe" -Force
|
||||
|
||||
# 2. Frontend + Tauri-App + NSIS-Installer
|
||||
pnpm tauri build
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Fertig. Installer:" -ForegroundColor Green
|
||||
Get-ChildItem "src-tauri/target/release/bundle/nsis/*.exe" | ForEach-Object { Write-Host " $($_.FullName)" }
|
||||
12
apps/rebreak-magic-win/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ReBreak Magic</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
26
apps/rebreak-magic-win/package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "rebreak-magic-win",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/nunito": "^5.1.0",
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.2.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
5051
apps/rebreak-magic-win/src-tauri/Cargo.lock
generated
Normal file
28
apps/rebreak-magic-win/src-tauri/Cargo.toml
Normal file
@ -0,0 +1,28 @@
|
||||
[workspace]
|
||||
members = ["protection-core", "service"]
|
||||
|
||||
[package]
|
||||
name = "rebreak-magic-win"
|
||||
version = "0.1.0"
|
||||
description = "ReBreak Magic für Windows — DNS-basierter Glücksspiel-Schutz"
|
||||
authors = ["Rebreak"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "rebreak_magic_win_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
keyring = { version = "3", features = ["windows-native", "apple-native"] }
|
||||
protection-core = { path = "protection-core" }
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
3
apps/rebreak-magic-win/src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capability für das Hauptfenster",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default"]
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default capability für das Hauptfenster","local":true,"windows":["main"],"permissions":["core:default"]}}
|
||||
2292
apps/rebreak-magic-win/src-tauri/gen/schemas/desktop-schema.json
Normal file
2292
apps/rebreak-magic-win/src-tauri/gen/schemas/macOS-schema.json
Normal file
BIN
apps/rebreak-magic-win/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
apps/rebreak-magic-win/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
apps/rebreak-magic-win/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
apps/rebreak-magic-win/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
apps/rebreak-magic-win/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
apps/rebreak-magic-win/src-tauri/icons/source.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "protection-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
270
apps/rebreak-magic-win/src-tauri/protection-core/src/lib.rs
Normal file
@ -0,0 +1,270 @@
|
||||
//! protection-core — geteilte DoH-Schutz-Logik für ReBreak Magic Windows.
|
||||
//!
|
||||
//! Wird von der Tauri-App (Setup via elevated One-Shot) UND vom
|
||||
//! Tamper-Protection-Service (SYSTEM, re-apply/teardown) benutzt.
|
||||
//!
|
||||
//! Architektur: Windows-DoH zeigt auf ReBreaks eigenen DoH-Server
|
||||
//! (dns.rebreak.org, AdGuard mit per-Device-Token im Pfad) — KEIN NextDNS.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
/// DoH-Host des ReBreak-DNS-Servers (AdGuard, kuratierte Gambling-Blocklist
|
||||
/// + User-Custom-Domains). Token wird als Pfad-Segment angehängt.
|
||||
pub const DOH_HOST: &str = "dns.rebreak.org";
|
||||
|
||||
/// Bootstrap-Fallback falls Resolve von DOH_HOST fehlschlägt
|
||||
/// (z.B. weil bereits ein kaputter DNS gesetzt ist). Hetzner-Box.
|
||||
pub const DOH_FALLBACK_IP: &str = "178.105.101.137";
|
||||
|
||||
/// Windows-Service-Name des Tamper-Protection-Monitors.
|
||||
pub const SERVICE_NAME: &str = "RebreakProtection";
|
||||
|
||||
/// Persistenter Zustand unter %ProgramData%\ReBreak\protection.json.
|
||||
/// Wird vom elevated Setup geschrieben, vom SYSTEM-Service gelesen.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProtectionState {
|
||||
/// Per-Device DNS-Token (64-char hex) aus POST /api/magic/register.
|
||||
pub dns_token: String,
|
||||
/// Aufgelöste IPv4 des DoH-Servers zum Setup-Zeitpunkt.
|
||||
pub server_ip: String,
|
||||
/// Vollständiges DoH-Template inkl. Token-Pfad.
|
||||
pub doh_template: String,
|
||||
/// Backend-Base-URL für Status-Polling (z.B. https://staging.rebreak.org).
|
||||
pub backend_url: String,
|
||||
/// Stabile Device-ID (win-<MachineGuid>).
|
||||
pub device_id: String,
|
||||
}
|
||||
|
||||
impl ProtectionState {
|
||||
pub fn doh_template_for(token: &str) -> String {
|
||||
format!("https://{DOH_HOST}/dns-query/{token}")
|
||||
}
|
||||
}
|
||||
|
||||
/// %ProgramData%\ReBreak — auf non-Windows (Dev) /tmp/rebreak-protection.
|
||||
pub fn state_dir() -> PathBuf {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let base = std::env::var("ProgramData").unwrap_or_else(|_| "C:\\ProgramData".into());
|
||||
PathBuf::from(base).join("ReBreak")
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
PathBuf::from("/tmp/rebreak-protection")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state_path() -> PathBuf {
|
||||
state_dir().join("protection.json")
|
||||
}
|
||||
|
||||
pub fn load_state() -> Option<ProtectionState> {
|
||||
let raw = std::fs::read_to_string(state_path()).ok()?;
|
||||
serde_json::from_str(&raw).ok()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PowerShell-Runner
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Führt ein PowerShell-Script aus (nicht-elevated, hidden window).
|
||||
/// Setup/Teardown brauchen Admin — dafür generiert die App ein One-Shot-Script
|
||||
/// und startet es via `Start-Process -Verb RunAs` (ein einziger UAC-Prompt).
|
||||
/// Der SYSTEM-Service ruft `ps_run` direkt auf (läuft bereits elevated).
|
||||
pub fn ps_run(script: &str) -> Result<String, String> {
|
||||
let mut cmd = Command::new("powershell.exe");
|
||||
cmd.args([
|
||||
"-NonInteractive",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
script,
|
||||
]);
|
||||
|
||||
// Kein Konsolen-Fenster aufpoppen lassen (CREATE_NO_WINDOW)
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
cmd.creation_flags(0x0800_0000);
|
||||
}
|
||||
|
||||
let out = cmd
|
||||
.output()
|
||||
.map_err(|e| format!("powershell.exe nicht startbar: {e}"))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
if out.status.success() {
|
||||
Ok(stdout)
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
Err(format!("PowerShell exit {:?}: {stderr}", out.status.code()))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Script-Generierung (pure Strings — cross-platform test- und reviewbar)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// DoH-Setup-Script (braucht Admin). Adaptiert aus dem verifizierten
|
||||
/// Windows-11-Flow: DoH-Server registrieren → Interface-DNS setzen →
|
||||
/// EnableAutoDoh → per-Interface DohFlags → flushdns.
|
||||
pub fn apply_script(server_ip: &str, doh_template: &str) -> String {
|
||||
format!(
|
||||
r#"$ErrorActionPreference = "Stop"
|
||||
$ip = "{server_ip}"
|
||||
$template = "{doh_template}"
|
||||
|
||||
# Schritt 1: ReBreak-DoH-Server als bekannten DoH-Endpoint registrieren
|
||||
Remove-DnsClientDohServerAddress -ServerAddress $ip -ErrorAction Ignore
|
||||
Add-DnsClientDohServerAddress `
|
||||
-ServerAddress $ip `
|
||||
-DohTemplate $template `
|
||||
-AllowFallbackToUdp $False `
|
||||
-AutoUpgrade $True
|
||||
|
||||
# Schritt 2: DNS aller aktiven (non-loopback) Adapter auf ReBreak setzen
|
||||
$adapters = Get-NetAdapter | Where-Object {{ $_.Status -eq "Up" -and -not $_.Virtual }}
|
||||
if (-not $adapters) {{ $adapters = Get-NetAdapter | Where-Object {{ $_.Status -eq "Up" }} }}
|
||||
foreach ($adapter in $adapters) {{
|
||||
Set-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex -ServerAddresses $ip
|
||||
|
||||
# Schritt 4: Interface-spezifische DoH-Flags (DoH erzwingen, kein Klartext-Fallback)
|
||||
$guid = $adapter.InterfaceGuid
|
||||
$dohPath = "HKLM:\System\CurrentControlSet\Services\Dnscache\InterfaceSpecificParameters\$guid\DohInterfaceSettings\Doh\$ip"
|
||||
New-Item -Path $dohPath -Force | Out-Null
|
||||
New-ItemProperty -Path $dohPath -Name "DohFlags" -Value 1 -PropertyType QWORD -Force | Out-Null
|
||||
}}
|
||||
|
||||
# Schritt 3: DoH systemweit erzwingen
|
||||
$regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters"
|
||||
Set-ItemProperty -Path $regPath -Name "EnableAutoDoh" -Value 2 -Type DWord
|
||||
|
||||
# Schritt 5: Browser-Policies — Chromium (Edge/Chrome/Brave) umgehen sonst die
|
||||
# System-DNS via eigenem built-in Resolver UND eigenem DoH. Beide MÜSSEN aus,
|
||||
# sonst greift die DNS-Sperre im Browser nicht (empirisch verifiziert 2026-06-07).
|
||||
# Test-Path-Guard statt New-Item -Force, damit bestehende Org-Policies bleiben.
|
||||
function Set-ChromiumDnsPolicy($base) {{
|
||||
if (-not (Test-Path $base)) {{ New-Item -Path $base -Force | Out-Null }}
|
||||
Set-ItemProperty -Path $base -Name "DnsOverHttpsMode" -Value "off" -Type String
|
||||
Set-ItemProperty -Path $base -Name "BuiltInDnsClientEnabled" -Value 0 -Type DWord
|
||||
}}
|
||||
Set-ChromiumDnsPolicy "HKLM:\SOFTWARE\Policies\Microsoft\Edge"
|
||||
Set-ChromiumDnsPolicy "HKLM:\SOFTWARE\Policies\Google\Chrome"
|
||||
Set-ChromiumDnsPolicy "HKLM:\SOFTWARE\Policies\BraveSoftware\Brave"
|
||||
# Firefox: eigenes DoH (TRR) aus + sperren (kein built-in-Resolver-Bypass)
|
||||
$ff = "HKLM:\SOFTWARE\Policies\Mozilla\Firefox\DNSOverHTTPS"
|
||||
if (-not (Test-Path $ff)) {{ New-Item -Path $ff -Force | Out-Null }}
|
||||
Set-ItemProperty -Path $ff -Name "Enabled" -Value 0 -Type DWord
|
||||
Set-ItemProperty -Path $ff -Name "Locked" -Value 1 -Type DWord
|
||||
|
||||
Clear-DnsClientCache
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// DoH-Teardown-Script (braucht Admin / SYSTEM). Wird NUR ausgeführt wenn
|
||||
/// das Backend den Token nach abgelaufenem 24h-Cooldown revoked hat.
|
||||
pub fn teardown_script(server_ip: &str) -> String {
|
||||
format!(
|
||||
r#"$ErrorActionPreference = "Continue"
|
||||
$ip = "{server_ip}"
|
||||
|
||||
Remove-DnsClientDohServerAddress -ServerAddress $ip -ErrorAction Ignore
|
||||
|
||||
$adapters = Get-NetAdapter | Where-Object {{ $_.Status -eq "Up" }}
|
||||
foreach ($adapter in $adapters) {{
|
||||
Set-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex -ResetServerAddresses
|
||||
$guid = $adapter.InterfaceGuid
|
||||
$dohPath = "HKLM:\System\CurrentControlSet\Services\Dnscache\InterfaceSpecificParameters\$guid\DohInterfaceSettings"
|
||||
Remove-Item -Path $dohPath -Recurse -ErrorAction Ignore
|
||||
}}
|
||||
|
||||
$regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters"
|
||||
Remove-ItemProperty -Path $regPath -Name "EnableAutoDoh" -ErrorAction Ignore
|
||||
|
||||
# Browser-Policies wieder entfernen
|
||||
foreach ($base in @("HKLM:\SOFTWARE\Policies\Microsoft\Edge","HKLM:\SOFTWARE\Policies\Google\Chrome","HKLM:\SOFTWARE\Policies\BraveSoftware\Brave")) {{
|
||||
Remove-ItemProperty -Path $base -Name "DnsOverHttpsMode" -ErrorAction Ignore
|
||||
Remove-ItemProperty -Path $base -Name "BuiltInDnsClientEnabled" -ErrorAction Ignore
|
||||
}}
|
||||
Remove-Item -Path "HKLM:\SOFTWARE\Policies\Mozilla\Firefox\DNSOverHTTPS" -Recurse -ErrorAction Ignore
|
||||
|
||||
Clear-DnsClientCache
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Status-Check-Script (läuft unelevated): prüft DoH-Registrierung UND
|
||||
/// dass mindestens ein aktiver Adapter ReBreak als DNS gesetzt hat.
|
||||
/// Output: "APPLIED" | "MISSING"
|
||||
pub fn check_script(server_ip: &str) -> String {
|
||||
format!(
|
||||
r#"$ip = "{server_ip}"
|
||||
$doh = Get-DnsClientDohServerAddress -ErrorAction Ignore | Where-Object {{ $_.ServerAddress -eq $ip }}
|
||||
$dns = Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction Ignore | Where-Object {{ $_.ServerAddresses -contains $ip }}
|
||||
if ($doh -and $dns) {{ Write-Output "APPLIED" }} else {{ Write-Output "MISSING" }}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// High-Level-API (nutzt ps_run — App-Check unelevated, Service elevated)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Prüft ob der DoH-Schutz aktuell greift (unelevated lesbar).
|
||||
pub fn doh_is_applied(server_ip: &str) -> bool {
|
||||
matches!(ps_run(&check_script(server_ip)), Ok(out) if out.trim() == "APPLIED")
|
||||
}
|
||||
|
||||
/// Wendet den DoH-Schutz an. Braucht Admin/SYSTEM-Kontext (Service).
|
||||
pub fn apply_doh(server_ip: &str, doh_template: &str) -> Result<(), String> {
|
||||
ps_run(&apply_script(server_ip, doh_template)).map(|_| ())
|
||||
}
|
||||
|
||||
/// Entfernt den DoH-Schutz. Braucht Admin/SYSTEM-Kontext (Service).
|
||||
pub fn teardown_doh(server_ip: &str) -> Result<(), String> {
|
||||
ps_run(&teardown_script(server_ip)).map(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn doh_template_embeds_token() {
|
||||
let t = ProtectionState::doh_template_for("abc123");
|
||||
assert_eq!(t, "https://dns.rebreak.org/dns-query/abc123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scripts_contain_ip_and_template() {
|
||||
let s = apply_script("1.2.3.4", "https://dns.rebreak.org/dns-query/tok");
|
||||
assert!(s.contains("1.2.3.4"));
|
||||
assert!(s.contains("/dns-query/tok"));
|
||||
assert!(s.contains("EnableAutoDoh"));
|
||||
let t = teardown_script("1.2.3.4");
|
||||
assert!(t.contains("ResetServerAddresses"));
|
||||
let c = check_script("1.2.3.4");
|
||||
assert!(c.contains("APPLIED"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_roundtrip() {
|
||||
let st = ProtectionState {
|
||||
dns_token: "tok".into(),
|
||||
server_ip: "1.2.3.4".into(),
|
||||
doh_template: "https://dns.rebreak.org/dns-query/tok".into(),
|
||||
backend_url: "https://staging.rebreak.org".into(),
|
||||
device_id: "win-x".into(),
|
||||
};
|
||||
let json = serde_json::to_string(&st).unwrap();
|
||||
let back: ProtectionState = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back.dns_token, "tok");
|
||||
// camelCase für Lesbarkeit im JSON-File
|
||||
assert!(json.contains("dnsToken"));
|
||||
}
|
||||
}
|
||||
14
apps/rebreak-magic-win/src-tauri/service/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "rebreak-protection-service"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "ReBreak Tamper-Protection-Service — DoH-Monitor (SYSTEM)"
|
||||
|
||||
[dependencies]
|
||||
protection-core = { path = "../protection-core" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-service = "0.7"
|
||||
201
apps/rebreak-magic-win/src-tauri/service/src/main.rs
Normal file
@ -0,0 +1,201 @@
|
||||
//! ReBreak Tamper-Protection-Service (Windows-Service, läuft als SYSTEM).
|
||||
//!
|
||||
//! Alle 5 Minuten:
|
||||
//! 1. Backend-Poll: GET /api/magic/status?token=<dnsToken>
|
||||
//! - active=true → DoH-Config prüfen, bei Manipulation sofort re-applien
|
||||
//! - active=false → 24h-Release-Cooldown ist durch: Teardown + Service disablen
|
||||
//! - offline → FAIL-CLOSED: Schutz bleibt, Manipulation wird trotzdem repariert
|
||||
//!
|
||||
//! Deaktivierung ist damit ausschließlich server-zeitgesteuert — die App
|
||||
//! (unelevated) kann den Schutz nicht abbauen, der User sieht keinen Code.
|
||||
//!
|
||||
//! Debug: `rebreak-protection-service.exe --console` läuft im Vordergrund.
|
||||
|
||||
use protection_core::{
|
||||
apply_doh, doh_is_applied, load_state, state_dir, teardown_doh, ProtectionState,
|
||||
SERVICE_NAME,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
const CHECK_INTERVAL: Duration = Duration::from_secs(5 * 60);
|
||||
const STARTUP_DELAY: Duration = Duration::from_secs(20);
|
||||
const HTTP_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
|
||||
fn log(msg: &str) {
|
||||
let line = format!(
|
||||
"[{}] {msg}\n",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0)
|
||||
);
|
||||
let path = state_dir().join("service.log");
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
|
||||
use std::io::Write;
|
||||
let _ = f.write_all(line.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
/// Backend-Status: Some(true)=aktiv, Some(false)=revoked, None=offline/Fehler.
|
||||
fn poll_backend(state: &ProtectionState) -> Option<bool> {
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.build()
|
||||
.ok()?;
|
||||
let url = format!(
|
||||
"{}/api/magic/status?token={}",
|
||||
state.backend_url.trim_end_matches('/'),
|
||||
state.dns_token
|
||||
);
|
||||
let res = client.get(&url).send().ok()?;
|
||||
if !res.status().is_success() {
|
||||
// 4xx/5xx ≠ "revoked" — nur explizites active=false zählt
|
||||
return None;
|
||||
}
|
||||
let body: serde_json::Value = res.json().ok()?;
|
||||
body.pointer("/data/active").and_then(|v| v.as_bool())
|
||||
}
|
||||
|
||||
/// Ein Monitor-Durchlauf. Rückgabe true = Service soll sich beenden (released).
|
||||
fn tick() -> bool {
|
||||
let Some(state) = load_state() else {
|
||||
log("Kein protection.json — idle.");
|
||||
return false;
|
||||
};
|
||||
|
||||
match poll_backend(&state) {
|
||||
Some(false) => {
|
||||
// Release-Cooldown abgelaufen, Token serverseitig revoked → Abbau
|
||||
log("Backend: Token revoked → Teardown.");
|
||||
match teardown_doh(&state.server_ip) {
|
||||
Ok(()) => {
|
||||
let _ = std::fs::remove_file(protection_core::state_path());
|
||||
// Autostart aus — Binary bleibt liegen bis App-Uninstall
|
||||
let _ = protection_core::ps_run(&format!(
|
||||
"sc.exe config {SERVICE_NAME} start= disabled"
|
||||
));
|
||||
log("Teardown ok — Service deaktiviert sich.");
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
log(&format!("Teardown fehlgeschlagen: {e}"));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
status => {
|
||||
// active=true ODER offline → fail-closed: Schutz sicherstellen
|
||||
if status.is_none() {
|
||||
log("Backend nicht erreichbar — fail-closed, Schutz bleibt.");
|
||||
}
|
||||
if !doh_is_applied(&state.server_ip) {
|
||||
log("Manipulation erkannt — DoH wird wiederhergestellt.");
|
||||
match apply_doh(&state.server_ip, &state.doh_template) {
|
||||
Ok(()) => log("DoH wiederhergestellt."),
|
||||
Err(e) => log(&format!("Re-Apply fehlgeschlagen: {e}")),
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Monitor-Loop; `stop_check` liefert true sobald der SCM Stop signalisiert.
|
||||
fn monitor_loop(stop_check: impl Fn(Duration) -> bool) {
|
||||
log("Service gestartet.");
|
||||
if stop_check(STARTUP_DELAY) {
|
||||
return;
|
||||
}
|
||||
loop {
|
||||
let done = tick();
|
||||
if done || stop_check(CHECK_INTERVAL) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
log("Service beendet.");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Windows-Service-Boilerplate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(windows)]
|
||||
mod win {
|
||||
use super::*;
|
||||
use std::ffi::OsString;
|
||||
use std::sync::mpsc;
|
||||
use windows_service::{
|
||||
define_windows_service,
|
||||
service::{
|
||||
ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState,
|
||||
ServiceStatus, ServiceType,
|
||||
},
|
||||
service_control_handler::{self, ServiceControlHandlerResult},
|
||||
service_dispatcher,
|
||||
};
|
||||
|
||||
define_windows_service!(ffi_service_main, service_main);
|
||||
|
||||
pub fn run() -> Result<(), windows_service::Error> {
|
||||
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
|
||||
}
|
||||
|
||||
fn service_main(_args: Vec<OsString>) {
|
||||
if let Err(e) = run_service() {
|
||||
log(&format!("Service-Fehler: {e:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
fn run_service() -> Result<(), windows_service::Error> {
|
||||
let (stop_tx, stop_rx) = mpsc::channel::<()>();
|
||||
|
||||
let handler = move |control| match control {
|
||||
ServiceControl::Stop | ServiceControl::Shutdown => {
|
||||
let _ = stop_tx.send(());
|
||||
ServiceControlHandlerResult::NoError
|
||||
}
|
||||
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
||||
_ => ServiceControlHandlerResult::NotImplemented,
|
||||
};
|
||||
let status = service_control_handler::register(SERVICE_NAME, handler)?;
|
||||
|
||||
let running = |state: ServiceState| ServiceStatus {
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
current_state: state,
|
||||
controls_accepted: ServiceControlAccept::STOP,
|
||||
exit_code: ServiceExitCode::Win32(0),
|
||||
checkpoint: 0,
|
||||
wait_hint: Duration::default(),
|
||||
process_id: None,
|
||||
};
|
||||
status.set_service_status(running(ServiceState::Running))?;
|
||||
|
||||
monitor_loop(|wait| stop_rx.recv_timeout(wait).is_ok());
|
||||
|
||||
status.set_service_status(running(ServiceState::Stopped))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// --console: Vordergrund-Modus für Debugging (auch unter macOS-Dev lauffähig)
|
||||
if std::env::args().any(|a| a == "--console") {
|
||||
monitor_loop(|wait| {
|
||||
std::thread::sleep(wait);
|
||||
false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Err(e) = win::run() {
|
||||
log(&format!("Dispatcher-Fehler: {e:?}"));
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
eprintln!("rebreak-protection-service läuft nur als Windows-Service. Nutze --console für Debug.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
166
apps/rebreak-magic-win/src-tauri/src/api.rs
Normal file
@ -0,0 +1,166 @@
|
||||
//! Backend-Client für /api/magic/* — alle HTTP-Calls laufen im Rust-Core.
|
||||
//! Der mgc_-Session-Token erreicht die WebView nie (analog Mac-Keychain-Modell).
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Envelope<T> {
|
||||
#[allow(dead_code)]
|
||||
success: bool,
|
||||
data: T,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ErrorBody {
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PairData {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MeData {
|
||||
pub nickname: Option<String>,
|
||||
pub plan: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterData {
|
||||
pub dns_token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MagicDevice {
|
||||
pub device_id: String,
|
||||
pub release_requested_at: Option<String>,
|
||||
pub release_available_at: Option<String>,
|
||||
}
|
||||
|
||||
pub struct Api {
|
||||
base: String,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl Api {
|
||||
pub fn new(base: &str) -> Self {
|
||||
Self {
|
||||
base: base.trim_end_matches('/').to_string(),
|
||||
client: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn parse<T: serde::de::DeserializeOwned>(
|
||||
res: reqwest::Response,
|
||||
) -> Result<T, String> {
|
||||
let status = res.status();
|
||||
let text = res.text().await.map_err(|e| e.to_string())?;
|
||||
if !status.is_success() {
|
||||
let msg = serde_json::from_str::<ErrorBody>(&text)
|
||||
.ok()
|
||||
.and_then(|b| b.message)
|
||||
.unwrap_or_else(|| format!("HTTP {status}"));
|
||||
return Err(msg);
|
||||
}
|
||||
serde_json::from_str::<Envelope<T>>(&text)
|
||||
.map(|e| e.data)
|
||||
.map_err(|e| format!("Unerwartete Server-Antwort: {e}"))
|
||||
}
|
||||
|
||||
/// POST /api/magic/pair/redeem — 6-stelliger Code → mgc_-Session-Token.
|
||||
pub async fn redeem_pair(&self, code: &str, label: &str) -> Result<PairData, String> {
|
||||
let res = self
|
||||
.client
|
||||
.post(format!("{}/api/magic/pair/redeem", self.base))
|
||||
.json(&json!({ "code": code, "label": label }))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Keine Verbindung zum Server: {e}"))?;
|
||||
Self::parse(res).await
|
||||
}
|
||||
|
||||
/// GET /api/magic/me — Hub-Header-Infos.
|
||||
pub async fn me(&self, token: &str) -> Result<MeData, String> {
|
||||
let res = self
|
||||
.client
|
||||
.get(format!("{}/api/magic/me", self.base))
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Keine Verbindung zum Server: {e}"))?;
|
||||
Self::parse(res).await
|
||||
}
|
||||
|
||||
/// POST /api/magic/register — DNS-Token + AdGuard-Provisioning (idempotent).
|
||||
pub async fn register(
|
||||
&self,
|
||||
token: &str,
|
||||
device_id: &str,
|
||||
hostname: &str,
|
||||
model: &str,
|
||||
os_version: &str,
|
||||
) -> Result<RegisterData, String> {
|
||||
let res = self
|
||||
.client
|
||||
.post(format!("{}/api/magic/register", self.base))
|
||||
.bearer_auth(token)
|
||||
.json(&json!({
|
||||
"deviceId": device_id,
|
||||
"hostname": hostname,
|
||||
"model": model,
|
||||
"osVersion": os_version,
|
||||
"platform": "windows",
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Keine Verbindung zum Server: {e}"))?;
|
||||
Self::parse(res).await
|
||||
}
|
||||
|
||||
/// GET /api/magic/devices — aktive Magic-Bindings des Users.
|
||||
pub async fn devices(&self, token: &str) -> Result<Vec<MagicDevice>, String> {
|
||||
let res = self
|
||||
.client
|
||||
.get(format!("{}/api/magic/devices", self.base))
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Keine Verbindung zum Server: {e}"))?;
|
||||
Self::parse(res).await
|
||||
}
|
||||
|
||||
/// POST /api/magic/devices/:id/request-release — startet 24h-Cooldown.
|
||||
pub async fn request_release(&self, token: &str, device_id: &str) -> Result<(), String> {
|
||||
let res = self
|
||||
.client
|
||||
.post(format!(
|
||||
"{}/api/magic/devices/{device_id}/request-release",
|
||||
self.base
|
||||
))
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Keine Verbindung zum Server: {e}"))?;
|
||||
Self::parse::<serde_json::Value>(res).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// POST /api/magic/devices/:id/cancel-release — bricht Cooldown ab.
|
||||
pub async fn cancel_release(&self, token: &str, device_id: &str) -> Result<(), String> {
|
||||
let res = self
|
||||
.client
|
||||
.post(format!(
|
||||
"{}/api/magic/devices/{device_id}/cancel-release",
|
||||
self.base
|
||||
))
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Keine Verbindung zum Server: {e}"))?;
|
||||
Self::parse::<serde_json::Value>(res).await.map(|_| ())
|
||||
}
|
||||
}
|
||||
42
apps/rebreak-magic-win/src-tauri/src/auth.rs
Normal file
@ -0,0 +1,42 @@
|
||||
//! Token-Storage im Windows Credential Manager (keyring-Crate).
|
||||
//! Analog zur Mac-App (Keychain): mgc_-Session-Token + DNS-Token
|
||||
//! liegen NIE im Frontend/localStorage.
|
||||
|
||||
use keyring::Entry;
|
||||
|
||||
const SERVICE: &str = "org.rebreak.magic";
|
||||
const SESSION_KEY: &str = "session-token";
|
||||
const DNS_KEY: &str = "dns-token";
|
||||
|
||||
fn entry(key: &str) -> Result<Entry, String> {
|
||||
Entry::new(SERVICE, key).map_err(|e| format!("Credential-Store-Fehler: {e}"))
|
||||
}
|
||||
|
||||
pub fn save_session_token(token: &str) -> Result<(), String> {
|
||||
entry(SESSION_KEY)?
|
||||
.set_password(token)
|
||||
.map_err(|e| format!("Token speichern fehlgeschlagen: {e}"))
|
||||
}
|
||||
|
||||
pub fn load_session_token() -> Option<String> {
|
||||
entry(SESSION_KEY).ok()?.get_password().ok()
|
||||
}
|
||||
|
||||
pub fn save_dns_token(token: &str) -> Result<(), String> {
|
||||
entry(DNS_KEY)?
|
||||
.set_password(token)
|
||||
.map_err(|e| format!("DNS-Token speichern fehlgeschlagen: {e}"))
|
||||
}
|
||||
|
||||
pub fn load_dns_token() -> Option<String> {
|
||||
entry(DNS_KEY).ok()?.get_password().ok()
|
||||
}
|
||||
|
||||
pub fn clear_all() {
|
||||
if let Ok(e) = entry(SESSION_KEY) {
|
||||
let _ = e.delete_credential();
|
||||
}
|
||||
if let Ok(e) = entry(DNS_KEY) {
|
||||
let _ = e.delete_credential();
|
||||
}
|
||||
}
|
||||
46
apps/rebreak-magic-win/src-tauri/src/device.rs
Normal file
@ -0,0 +1,46 @@
|
||||
//! Stabile Device-Identität — analog Mac-App (Hardware-UUID),
|
||||
//! auf Windows via MachineGuid aus der Registry.
|
||||
|
||||
#[cfg(windows)]
|
||||
use protection_core::ps_run;
|
||||
|
||||
/// Stabile Device-ID: "win-<MachineGuid>".
|
||||
/// MachineGuid übersteht Reinstalls der App (nicht aber Windows-Neuinstall).
|
||||
pub fn device_id() -> String {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Ok(out) = ps_run(
|
||||
"(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Cryptography').MachineGuid",
|
||||
) {
|
||||
let guid = out.trim();
|
||||
if !guid.is_empty() {
|
||||
return format!("win-{guid}");
|
||||
}
|
||||
}
|
||||
// Fallback: Hostname-basiert (sollte praktisch nie passieren)
|
||||
format!("win-{}", hostname())
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
format!("win-dev-{}", hostname())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hostname() -> String {
|
||||
std::env::var("COMPUTERNAME")
|
||||
.or_else(|_| std::env::var("HOSTNAME"))
|
||||
.unwrap_or_else(|_| "Windows-PC".into())
|
||||
}
|
||||
|
||||
pub fn os_version() -> String {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
ps_run("[System.Environment]::OSVersion.VersionString")
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|_| "Windows".into())
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
"dev".into()
|
||||
}
|
||||
}
|
||||
198
apps/rebreak-magic-win/src-tauri/src/lib.rs
Normal file
@ -0,0 +1,198 @@
|
||||
//! ReBreak Magic für Windows — Tauri-Core.
|
||||
//!
|
||||
//! Alle Backend-Calls + Token-Handling laufen hier in Rust; die WebView
|
||||
//! sieht nur den aggregierten UI-State (nie Tokens). Analog zur Mac-App
|
||||
//! (rebreak-magic-mac), gleiche /api/magic/*-Endpoints.
|
||||
|
||||
mod api;
|
||||
mod auth;
|
||||
mod device;
|
||||
mod setup;
|
||||
|
||||
use api::Api;
|
||||
use protection_core::ProtectionState;
|
||||
use serde::Serialize;
|
||||
use tauri::Manager;
|
||||
|
||||
const DEFAULT_BACKEND_URL: &str = "https://staging.rebreak.org";
|
||||
|
||||
#[derive(Serialize, Clone, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UiState {
|
||||
backend_url: String,
|
||||
logged_in: bool,
|
||||
nickname: Option<String>,
|
||||
plan: Option<String>,
|
||||
device_id: String,
|
||||
hostname: String,
|
||||
/// Magic-Binding existiert (DNS-Token provisioniert)
|
||||
registered: bool,
|
||||
/// Windows-DoH zeigt aktuell auf ReBreak (lokaler Check)
|
||||
protection_applied: bool,
|
||||
release_requested_at: Option<String>,
|
||||
release_available_at: Option<String>,
|
||||
}
|
||||
|
||||
/// Backend-URL: config.json im App-Config-Dir > Default (Staging).
|
||||
/// Gleiche Semantik wie config.example.json der Mac-App.
|
||||
fn backend_url(app: &tauri::AppHandle) -> String {
|
||||
if let Ok(dir) = app.path().app_config_dir() {
|
||||
if let Ok(raw) = std::fs::read_to_string(dir.join("config.json")) {
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) {
|
||||
if let Some(url) = v.get("backendBaseUrl").and_then(|u| u.as_str()) {
|
||||
return url.trim_end_matches('/').to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DEFAULT_BACKEND_URL.into()
|
||||
}
|
||||
|
||||
/// Aggregiert den kompletten UI-State (ein Roundtrip fürs Frontend).
|
||||
async fn build_state(app: &tauri::AppHandle) -> UiState {
|
||||
let mut state = UiState {
|
||||
backend_url: backend_url(app),
|
||||
device_id: device::device_id(),
|
||||
hostname: device::hostname(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Lokaler DoH-Check ist auth-unabhängig
|
||||
let server_ip = protection_core::load_state()
|
||||
.map(|s| s.server_ip)
|
||||
.unwrap_or_else(setup::resolve_doh_ip);
|
||||
state.protection_applied = protection_core::doh_is_applied(&server_ip);
|
||||
|
||||
let Some(token) = auth::load_session_token() else {
|
||||
return state;
|
||||
};
|
||||
|
||||
let api = Api::new(&state.backend_url);
|
||||
match api.me(&token).await {
|
||||
Ok(me) => {
|
||||
state.logged_in = true;
|
||||
state.nickname = me.nickname;
|
||||
state.plan = me.plan;
|
||||
}
|
||||
Err(_) => {
|
||||
// Token abgelaufen/revoked → wie ausgeloggt behandeln
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(devices) = api.devices(&token).await {
|
||||
if let Some(mine) = devices.iter().find(|d| d.device_id == state.device_id) {
|
||||
state.registered = true;
|
||||
state.release_requested_at = mine.release_requested_at.clone();
|
||||
state.release_available_at = mine.release_available_at.clone();
|
||||
}
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tauri-Commands — jedes gibt den frischen UiState zurück
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_state(app: tauri::AppHandle) -> Result<UiState, String> {
|
||||
Ok(build_state(&app).await)
|
||||
}
|
||||
|
||||
/// Pairing-Code einlösen + Device registrieren (ein Schritt fürs UI).
|
||||
#[tauri::command]
|
||||
async fn pair_and_register(app: tauri::AppHandle, code: String) -> Result<UiState, String> {
|
||||
let code = code.trim().to_string();
|
||||
if !code.chars().all(|c| c.is_ascii_digit()) || code.len() != 6 {
|
||||
return Err("Der Code besteht aus 6 Ziffern.".into());
|
||||
}
|
||||
|
||||
let base = backend_url(&app);
|
||||
let api = Api::new(&base);
|
||||
let hostname = device::hostname();
|
||||
|
||||
let pair = api.redeem_pair(&code, &hostname).await?;
|
||||
auth::save_session_token(&pair.token)?;
|
||||
|
||||
let reg = api
|
||||
.register(
|
||||
&pair.token,
|
||||
&device::device_id(),
|
||||
&hostname,
|
||||
"Windows-PC",
|
||||
&device::os_version(),
|
||||
)
|
||||
.await?;
|
||||
auth::save_dns_token(®.dns_token)?;
|
||||
|
||||
Ok(build_state(&app).await)
|
||||
}
|
||||
|
||||
/// Schutz aktivieren: EIN UAC-Prompt → DoH + State + Tamper-Service.
|
||||
#[tauri::command]
|
||||
async fn activate_protection(app: tauri::AppHandle) -> Result<UiState, String> {
|
||||
let dns_token = auth::load_dns_token()
|
||||
.ok_or_else(|| "Kein DNS-Token — bitte zuerst koppeln.".to_string())?;
|
||||
|
||||
let server_ip = setup::resolve_doh_ip();
|
||||
let state = ProtectionState {
|
||||
doh_template: ProtectionState::doh_template_for(&dns_token),
|
||||
dns_token,
|
||||
server_ip,
|
||||
backend_url: backend_url(&app),
|
||||
device_id: device::device_id(),
|
||||
};
|
||||
|
||||
let service_exe = setup::service_exe_path()?;
|
||||
let script = setup::build_setup_script(&state, &service_exe.display().to_string());
|
||||
setup::run_elevated(&script)?;
|
||||
|
||||
let ui = build_state(&app).await;
|
||||
if !ui.protection_applied {
|
||||
return Err("Setup lief durch, aber der DoH-Schutz ist nicht aktiv. Bitte erneut versuchen.".into());
|
||||
}
|
||||
Ok(ui)
|
||||
}
|
||||
|
||||
/// 24h-Cooldown starten. Teardown macht NICHT die App — der SYSTEM-Service
|
||||
/// pollt /api/magic/status und baut erst ab wenn das Backend revoked.
|
||||
#[tauri::command]
|
||||
async fn request_release(app: tauri::AppHandle) -> Result<UiState, String> {
|
||||
let token = auth::load_session_token().ok_or("Nicht eingeloggt")?;
|
||||
let api = Api::new(&backend_url(&app));
|
||||
api.request_release(&token, &device::device_id()).await?;
|
||||
Ok(build_state(&app).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cancel_release(app: tauri::AppHandle) -> Result<UiState, String> {
|
||||
let token = auth::load_session_token().ok_or("Nicht eingeloggt")?;
|
||||
let api = Api::new(&backend_url(&app));
|
||||
api.cancel_release(&token, &device::device_id()).await?;
|
||||
Ok(build_state(&app).await)
|
||||
}
|
||||
|
||||
/// Logout entfernt nur die Session — Schutz + Service bleiben aktiv
|
||||
/// (Deaktivierung geht ausschließlich über den 24h-Release-Flow).
|
||||
#[tauri::command]
|
||||
async fn logout(app: tauri::AppHandle) -> Result<UiState, String> {
|
||||
auth::clear_all();
|
||||
Ok(build_state(&app).await)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_state,
|
||||
pair_and_register,
|
||||
activate_protection,
|
||||
request_release,
|
||||
cancel_release,
|
||||
logout,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
6
apps/rebreak-magic-win/src-tauri/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
rebreak_magic_win_lib::run()
|
||||
}
|
||||
95
apps/rebreak-magic-win/src-tauri/src/setup.rs
Normal file
@ -0,0 +1,95 @@
|
||||
//! Elevated One-Shot-Setup: EIN UAC-Prompt erledigt alles —
|
||||
//! DoH-Konfiguration, ProgramData-State, Tamper-Service-Installation.
|
||||
//! Danach läuft alles silent (Service als SYSTEM, Deaktivierung server-gesteuert).
|
||||
|
||||
use protection_core::{apply_script, ProtectionState, SERVICE_NAME};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use protection_core::{ps_run, DOH_FALLBACK_IP, DOH_HOST};
|
||||
|
||||
/// Löst die IPv4 des DoH-Hosts auf (solange DNS noch normal funktioniert).
|
||||
/// Fallback auf die bekannte Hetzner-IP wenn Resolve fehlschlägt.
|
||||
pub fn resolve_doh_ip() -> String {
|
||||
if let Ok(addrs) = format!("{DOH_HOST}:443").to_socket_addrs() {
|
||||
for addr in addrs {
|
||||
if let std::net::SocketAddr::V4(v4) = addr {
|
||||
return v4.ip().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
DOH_FALLBACK_IP.to_string()
|
||||
}
|
||||
|
||||
/// Pfad des Tamper-Service-Binaries (Tauri-Sidecar, liegt neben der Haupt-Exe).
|
||||
pub fn service_exe_path() -> Result<PathBuf, String> {
|
||||
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
|
||||
let dir = exe
|
||||
.parent()
|
||||
.ok_or_else(|| "Exe-Verzeichnis nicht ermittelbar".to_string())?;
|
||||
Ok(dir.join("rebreak-protection-service.exe"))
|
||||
}
|
||||
|
||||
/// Baut das komplette elevated Setup-Script:
|
||||
/// 1. DoH anwenden 2. State nach %ProgramData%\ReBreak 3. Service installieren+starten
|
||||
pub fn build_setup_script(state: &ProtectionState, service_exe: &str) -> String {
|
||||
let mut script = String::new();
|
||||
|
||||
script.push_str(&apply_script(&state.server_ip, &state.doh_template));
|
||||
|
||||
let state_dir = protection_core::state_dir();
|
||||
let state_path = protection_core::state_path();
|
||||
let json = serde_json::to_string_pretty(state).unwrap_or_default();
|
||||
|
||||
script.push_str(&format!(
|
||||
r#"
|
||||
# Persistenter State für den Tamper-Service (SYSTEM liest das beim Polling)
|
||||
New-Item -ItemType Directory -Force -Path '{state_dir}' | Out-Null
|
||||
@'
|
||||
{json}
|
||||
'@ | Set-Content -Path '{state_path}' -Encoding UTF8
|
||||
|
||||
# Tamper-Protection-Service als SYSTEM installieren (idempotent: alte Instanz weg)
|
||||
sc.exe stop {SERVICE_NAME} 2>$null | Out-Null
|
||||
sc.exe delete {SERVICE_NAME} 2>$null | Out-Null
|
||||
sc.exe create {SERVICE_NAME} binPath= '"{service_exe}"' start= auto obj= LocalSystem DisplayName= "ReBreak Protection" | Out-Null
|
||||
sc.exe description {SERVICE_NAME} "ReBreak DNS-Schutz — Tamper-Monitor (alle 5 Minuten)" | Out-Null
|
||||
sc.exe failure {SERVICE_NAME} reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null
|
||||
sc.exe start {SERVICE_NAME} | Out-Null
|
||||
|
||||
exit 0
|
||||
"#,
|
||||
state_dir = state_dir.display(),
|
||||
state_path = state_path.display(),
|
||||
));
|
||||
|
||||
script
|
||||
}
|
||||
|
||||
/// Schreibt das Script als Temp-.ps1 und startet es elevated (UAC) mit -Wait.
|
||||
/// Gibt Err zurück wenn der User den UAC-Prompt abbricht oder das Script failt.
|
||||
pub fn run_elevated(script: &str) -> Result<(), String> {
|
||||
let tmp = std::env::temp_dir().join(format!(
|
||||
"rebreak-setup-{}.ps1",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::write(&tmp, script).map_err(|e| format!("Temp-Script: {e}"))?;
|
||||
|
||||
// Outer-PS startet Inner-PS elevated; -Wait + -PassThru für Exit-Code
|
||||
let outer = format!(
|
||||
r#"$p = Start-Process powershell.exe -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NonInteractive','-NoProfile','-ExecutionPolicy','Bypass','-File','{}')
|
||||
exit $p.ExitCode"#,
|
||||
tmp.display()
|
||||
);
|
||||
|
||||
let result = ps_run(&outer);
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) if e.contains("canceled") || e.contains("abgebrochen") => {
|
||||
Err("Administrator-Bestätigung wurde abgebrochen.".into())
|
||||
}
|
||||
Err(e) => Err(format!("Setup fehlgeschlagen: {e}")),
|
||||
}
|
||||
}
|
||||
46
apps/rebreak-magic-win/src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ReBreak Magic",
|
||||
"version": "0.1.0",
|
||||
"identifier": "org.rebreak.magic",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "ReBreak Magic",
|
||||
"width": 780,
|
||||
"height": 600,
|
||||
"minWidth": 720,
|
||||
"minHeight": 540,
|
||||
"center": true,
|
||||
"resizable": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["nsis"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.ico",
|
||||
"icons/icon.png"
|
||||
],
|
||||
"externalBin": ["binaries/rebreak-protection-service"],
|
||||
"windows": {
|
||||
"nsis": {
|
||||
"installMode": "perMachine",
|
||||
"languages": ["German", "English"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
apps/rebreak-magic-win/src/App.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { getState, UiState } from "./lib/ipc";
|
||||
import LoginView from "./views/LoginView";
|
||||
import HubView from "./views/HubView";
|
||||
|
||||
export default function App() {
|
||||
const [state, setState] = useState<UiState | null>(null);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
setState(await getState());
|
||||
setLoadError(null);
|
||||
} catch (e) {
|
||||
setLoadError(String(e));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
// Periodischer Refresh: spiegelt Server-State (z.B. Release durch Cron)
|
||||
const id = setInterval(refresh, 60_000);
|
||||
return () => clearInterval(id);
|
||||
}, [refresh]);
|
||||
|
||||
if (!state) {
|
||||
return (
|
||||
<div className="app">
|
||||
<div className="center-fill">
|
||||
{loadError ? <p className="error">{loadError}</p> : <div className="spin" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
{state.loggedIn ? (
|
||||
<HubView state={state} onState={setState} />
|
||||
) : (
|
||||
<LoginView onState={setState} />
|
||||
)}
|
||||
<div className="footer">
|
||||
<span>ReBreak Magic für Windows</span>
|
||||
<span>{state.hostname}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
apps/rebreak-magic-win/src/assets/app-icon.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
23
apps/rebreak-magic-win/src/lib/ipc.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
/** Spiegelbild von UiState in src-tauri/src/lib.rs */
|
||||
export interface UiState {
|
||||
backendUrl: string;
|
||||
loggedIn: boolean;
|
||||
nickname: string | null;
|
||||
plan: string | null;
|
||||
deviceId: string;
|
||||
hostname: string;
|
||||
registered: boolean;
|
||||
protectionApplied: boolean;
|
||||
releaseRequestedAt: string | null;
|
||||
releaseAvailableAt: string | null;
|
||||
}
|
||||
|
||||
export const getState = () => invoke<UiState>("get_state");
|
||||
export const pairAndRegister = (code: string) =>
|
||||
invoke<UiState>("pair_and_register", { code });
|
||||
export const activateProtection = () => invoke<UiState>("activate_protection");
|
||||
export const requestRelease = () => invoke<UiState>("request_release");
|
||||
export const cancelRelease = () => invoke<UiState>("cancel_release");
|
||||
export const logout = () => invoke<UiState>("logout");
|
||||
14
apps/rebreak-magic-win/src/main.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "@fontsource/nunito/400.css";
|
||||
import "@fontsource/nunito/600.css";
|
||||
import "@fontsource/nunito/700.css";
|
||||
import "@fontsource/nunito/800.css";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
308
apps/rebreak-magic-win/src/styles.css
Normal file
@ -0,0 +1,308 @@
|
||||
/* ReBreak Magic — Design-Tokens analog rebreak-magic-mac + rebreak-native */
|
||||
:root {
|
||||
--accent: #2e7fd4; /* AccentColor der Mac-App (0.180/0.498/0.831) */
|
||||
--accent-soft: rgba(46, 127, 212, 0.08);
|
||||
--navy: #1f3a5c; /* Wordmark-Navy aus dem App-Icon */
|
||||
--bg: #f5f5f7; /* windowBackgroundColor */
|
||||
--card: #ffffff; /* controlBackgroundColor */
|
||||
--text: #1c1c1e;
|
||||
--text-secondary: #6e6e73;
|
||||
--text-tertiary: #aeaeb2;
|
||||
--green: #2e9e5b;
|
||||
--red: #d4452e;
|
||||
--orange: #d4882e;
|
||||
--border: rgba(0, 0, 0, 0.08);
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Nunito", "Segoe UI", system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.app {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ---------- Header (Hub) ---------- */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 20px;
|
||||
background: var(--card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.header img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.header .wordmark {
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: var(--navy);
|
||||
}
|
||||
|
||||
.header .spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.plan-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
border-radius: 6px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ---------- Layout ---------- */
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card .sub {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* ---------- Status ---------- */
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-dot.on {
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 0 4px rgba(46, 158, 91, 0.15);
|
||||
}
|
||||
|
||||
.status-dot.off {
|
||||
background: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.status-dot.cooldown {
|
||||
background: var(--orange);
|
||||
box-shadow: 0 0 0 4px rgba(212, 136, 46, 0.15);
|
||||
}
|
||||
|
||||
/* ---------- Buttons ---------- */
|
||||
button {
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
border-radius: 9px;
|
||||
padding: 10px 18px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.12s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-danger-quiet {
|
||||
background: transparent;
|
||||
color: var(--red);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
/* ---------- Login ---------- */
|
||||
.login {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login img {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.login h1 {
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: var(--navy);
|
||||
}
|
||||
|
||||
.login .lead {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
max-width: 400px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.code-input {
|
||||
font-family: inherit;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 14px;
|
||||
text-align: center;
|
||||
text-indent: 14px; /* gleicht letter-spacing-Versatz aus */
|
||||
width: 260px;
|
||||
padding: 12px 0;
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.code-input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
max-width: 380px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--red);
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
/* ---------- Countdown ---------- */
|
||||
.countdown {
|
||||
font-size: 34px;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--orange);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.row.end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ---------- Footer ---------- */
|
||||
.footer {
|
||||
padding: 10px 20px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.spin {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2.5px solid var(--accent-soft);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.center-fill {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
161
apps/rebreak-magic-win/src/views/HubView.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
activateProtection,
|
||||
cancelRelease,
|
||||
logout,
|
||||
requestRelease,
|
||||
UiState,
|
||||
} from "../lib/ipc";
|
||||
import appIcon from "../assets/app-icon.png";
|
||||
|
||||
interface Props {
|
||||
state: UiState;
|
||||
onState: (s: UiState) => void;
|
||||
}
|
||||
|
||||
function useCountdown(targetIso: string | null): string | null {
|
||||
const target = useMemo(
|
||||
() => (targetIso ? new Date(targetIso).getTime() : null),
|
||||
[targetIso],
|
||||
);
|
||||
const [now, setNow] = useState(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (target === null) return;
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [target]);
|
||||
|
||||
if (target === null) return null;
|
||||
const ms = Math.max(0, target - now);
|
||||
const h = Math.floor(ms / 3_600_000);
|
||||
const m = Math.floor((ms % 3_600_000) / 60_000);
|
||||
const s = Math.floor((ms % 60_000) / 1000);
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${pad(h)}:${pad(m)}:${pad(s)}`;
|
||||
}
|
||||
|
||||
export default function HubView({ state, onState }: Props) {
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const countdown = useCountdown(state.releaseAvailableAt);
|
||||
|
||||
const run = async (key: string, fn: () => Promise<UiState>) => {
|
||||
setBusy(key);
|
||||
setError(null);
|
||||
try {
|
||||
onState(await fn());
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
const cooldownActive = state.releaseRequestedAt !== null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="header">
|
||||
<img src={appIcon} alt="" />
|
||||
<span className="wordmark">ReBreak Magic</span>
|
||||
<div className="spacer" />
|
||||
{state.plan && <span className="plan-badge">{state.plan}</span>}
|
||||
{state.nickname && <span className="nickname">{state.nickname}</span>}
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{/* ---- Schutz-Status ---- */}
|
||||
<div className="card">
|
||||
<div className="status-row">
|
||||
<div
|
||||
className={`status-dot ${
|
||||
cooldownActive ? "cooldown" : state.protectionApplied ? "on" : "off"
|
||||
}`}
|
||||
/>
|
||||
<div className="grow">
|
||||
<h2>
|
||||
{cooldownActive
|
||||
? "Schutz wird aufgehoben"
|
||||
: state.protectionApplied
|
||||
? "Schutz aktiv"
|
||||
: "Schutz nicht aktiv"}
|
||||
</h2>
|
||||
<p className="sub">
|
||||
{cooldownActive
|
||||
? "Der 24-Stunden-Cooldown läuft. Bis dahin bleibt der Schutz vollständig aktiv."
|
||||
: state.protectionApplied
|
||||
? "Glücksspielseiten werden auf diesem PC systemweit über ReBreak-DNS blockiert."
|
||||
: "Aktiviere den DNS-Schutz für diesen PC. Windows fragt dabei einmalig nach Administrator-Rechten."}
|
||||
</p>
|
||||
</div>
|
||||
{!state.protectionApplied && !cooldownActive && (
|
||||
<button
|
||||
className="btn-primary"
|
||||
disabled={busy !== null || !state.registered}
|
||||
onClick={() => run("activate", activateProtection)}
|
||||
>
|
||||
{busy === "activate" ? "Wird eingerichtet …" : "Schutz aktivieren"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---- Cooldown ---- */}
|
||||
{cooldownActive && (
|
||||
<div className="card">
|
||||
<h2>Verbleibende Zeit</h2>
|
||||
<p className="sub">
|
||||
Danach hebt ReBreak den Schutz auf diesem PC automatisch auf.
|
||||
Du kannst es dir jederzeit anders überlegen.
|
||||
</p>
|
||||
<div className="row" style={{ marginTop: 12 }}>
|
||||
<span className="countdown">{countdown ?? "—"}</span>
|
||||
<div className="grow" />
|
||||
<button
|
||||
className="btn-secondary"
|
||||
disabled={busy !== null}
|
||||
onClick={() => run("cancel", cancelRelease)}
|
||||
>
|
||||
{busy === "cancel" ? "…" : "Schutz behalten"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ---- Gerät ---- */}
|
||||
<div className="card">
|
||||
<h2>Dieses Gerät</h2>
|
||||
<p className="sub">
|
||||
{state.hostname}
|
||||
{state.registered
|
||||
? " — mit deinem ReBreak-Konto verbunden."
|
||||
: " — noch nicht registriert."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
{/* ---- Aktionen ---- */}
|
||||
<div className="row end">
|
||||
{state.protectionApplied && !cooldownActive && (
|
||||
<button
|
||||
className="btn-danger-quiet"
|
||||
disabled={busy !== null}
|
||||
onClick={() => run("release", requestRelease)}
|
||||
>
|
||||
{busy === "release" ? "…" : "Schutz pausieren (24h-Cooldown)"}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn-link"
|
||||
disabled={busy !== null}
|
||||
onClick={() => run("logout", logout)}
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
70
apps/rebreak-magic-win/src/views/LoginView.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { useState } from "react";
|
||||
import { pairAndRegister, UiState } from "../lib/ipc";
|
||||
import appIcon from "../assets/app-icon.png";
|
||||
|
||||
interface Props {
|
||||
onState: (s: UiState) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pairing-Flow analog zur Mac-App: 6-stelliger Code aus der ReBreak-App
|
||||
* (Geräte → "Computer koppeln"), kein Passwort, kein Account-Formular.
|
||||
*/
|
||||
export default function LoginView({ onState }: Props) {
|
||||
const [code, setCode] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const submit = async () => {
|
||||
if (code.length !== 6 || busy) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
onState(await pairAndRegister(code));
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
setCode("");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login">
|
||||
<img src={appIcon} alt="" />
|
||||
<h1>ReBreak Magic</h1>
|
||||
<p className="lead">
|
||||
Schütze diesen Windows-PC vor Glücksspielseiten. Öffne die
|
||||
ReBreak-App auf deinem Handy und erstelle unter{" "}
|
||||
<strong>Geräte → Computer koppeln</strong> einen Kopplungs-Code.
|
||||
</p>
|
||||
|
||||
<input
|
||||
className="code-input"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoFocus
|
||||
maxLength={6}
|
||||
placeholder="······"
|
||||
value={code}
|
||||
disabled={busy}
|
||||
onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
|
||||
onKeyDown={(e) => e.key === "Enter" && submit()}
|
||||
/>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
<button
|
||||
className="btn-primary"
|
||||
disabled={code.length !== 6 || busy}
|
||||
onClick={submit}
|
||||
>
|
||||
{busy ? "Wird gekoppelt …" : "Koppeln"}
|
||||
</button>
|
||||
|
||||
<p className="hint">
|
||||
Der Code ist 10 Minuten gültig und kann nur einmal verwendet werden.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
apps/rebreak-magic-win/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
20
apps/rebreak-magic-win/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
17
apps/rebreak-magic-win/vite.config.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// Tauri-Dev-Setup: fester Port, kein clearScreen (Rust-Logs sichtbar lassen)
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
},
|
||||
build: {
|
||||
// WebView2 ist Chromium — moderne Targets ok
|
||||
target: "chrome105",
|
||||
outDir: "dist",
|
||||
},
|
||||
});
|
||||
396
pnpm-lock.yaml
generated
@ -121,6 +121,40 @@ importers:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
|
||||
apps/rebreak-magic-win:
|
||||
dependencies:
|
||||
'@fontsource/nunito':
|
||||
specifier: ^5.1.0
|
||||
version: 5.2.7
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.2.0
|
||||
version: 2.11.0
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
react-dom:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
devDependencies:
|
||||
'@tauri-apps/cli':
|
||||
specifier: ^2.2.0
|
||||
version: 2.11.2
|
||||
'@types/react':
|
||||
specifier: ^18.3.12
|
||||
version: 18.3.31
|
||||
'@types/react-dom':
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.7(@types/react@18.3.31)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.3.4
|
||||
version: 4.7.0(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))
|
||||
typescript:
|
||||
specifier: ^5.7.2
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^6.0.7
|
||||
version: 6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
|
||||
apps/rebreak-native:
|
||||
dependencies:
|
||||
'@config-plugins/react-native-callkeep':
|
||||
@ -236,7 +270,7 @@ importers:
|
||||
version: 0.32.17(expo@54.0.34)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
|
||||
expo-router:
|
||||
specifier: ~6.0.23
|
||||
version: 6.0.23(@expo/metro-runtime@6.1.2)(@types/react@19.2.14)(expo-constants@18.0.13)(expo-linking@8.0.12)(expo@54.0.34)(react-dom@19.1.0(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)
|
||||
version: 6.0.23(@expo/metro-runtime@6.1.2)(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(expo-constants@18.0.13)(expo-linking@8.0.12)(expo@54.0.34)(react-dom@19.1.0(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)
|
||||
expo-speech:
|
||||
specifier: ~14.0.8
|
||||
version: 14.0.8(expo@54.0.34)
|
||||
@ -351,7 +385,7 @@ importers:
|
||||
version: 7.8.0
|
||||
'@prisma/client':
|
||||
specifier: ^7.2.0
|
||||
version: 7.8.0(prisma@7.8.0(@types/react@19.2.14)(magicast@0.5.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3))(typescript@5.9.3)
|
||||
version: 7.8.0(prisma@7.8.0(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(magicast@0.5.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3))(typescript@5.9.3)
|
||||
'@supabase/supabase-js':
|
||||
specifier: ^2.39.7
|
||||
version: 2.105.3
|
||||
@ -403,7 +437,7 @@ importers:
|
||||
version: 2.13.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3)(oxc-parser@0.132.0)
|
||||
prisma:
|
||||
specifier: ^7.2.0
|
||||
version: 7.8.0(@types/react@19.2.14)(magicast@0.5.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3)
|
||||
version: 7.8.0(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(magicast@0.5.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
@ -1813,6 +1847,9 @@ packages:
|
||||
'@floating-ui/vue@1.1.11':
|
||||
resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==}
|
||||
|
||||
'@fontsource/nunito@5.2.7':
|
||||
resolution: {integrity: sha512-pmtBq0H9ex9nk+RtJYEJOD9pag393iHETnl/PVKleF4i06cd0ttngK5ZCTgYb5eOqR3Xdlrjtev8m7bmgYprew==}
|
||||
|
||||
'@hono/node-server@1.19.11':
|
||||
resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
@ -3263,6 +3300,9 @@ packages:
|
||||
'@rolldown/pluginutils@1.0.0':
|
||||
resolution: {integrity: sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27':
|
||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-rc.13':
|
||||
resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==}
|
||||
|
||||
@ -3658,6 +3698,80 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^2.7.0 || ^3.0.0
|
||||
|
||||
'@tauri-apps/api@2.11.0':
|
||||
resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@2.11.2':
|
||||
resolution: {integrity: sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tauri-apps/cli-darwin-x64@2.11.2':
|
||||
resolution: {integrity: sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf@2.11.2':
|
||||
resolution: {integrity: sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-gnu@2.11.2':
|
||||
resolution: {integrity: sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.11.2':
|
||||
resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-linux-riscv64-gnu@2.11.2':
|
||||
resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.11.2':
|
||||
resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@2.11.2':
|
||||
resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.11.2':
|
||||
resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/cli-win32-ia32-msvc@2.11.2':
|
||||
resolution: {integrity: sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/cli-win32-x64-msvc@2.11.2':
|
||||
resolution: {integrity: sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/cli@2.11.2':
|
||||
resolution: {integrity: sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==}
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
'@tiptap/core@3.23.1':
|
||||
resolution: {integrity: sha512-8YvSGiJTeU5wPuGiYIIYgyiyaaT1CAx+kJL0bju0w871OvbJJj0T/ywhcmxGXW6pOal2T8X2xt9ZqE+vib0VJw==}
|
||||
peerDependencies:
|
||||
@ -3931,6 +4045,17 @@ packages:
|
||||
'@types/pg@8.20.0':
|
||||
resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==}
|
||||
|
||||
'@types/prop-types@15.7.15':
|
||||
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
|
||||
|
||||
'@types/react-dom@18.3.7':
|
||||
resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
|
||||
peerDependencies:
|
||||
'@types/react': ^18.0.0
|
||||
|
||||
'@types/react@18.3.31':
|
||||
resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==}
|
||||
|
||||
'@types/react@19.2.14':
|
||||
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
||||
|
||||
@ -4025,6 +4150,12 @@ packages:
|
||||
peerDependencies:
|
||||
vite: '*'
|
||||
|
||||
'@vitejs/plugin-react@4.7.0':
|
||||
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
|
||||
'@vitejs/plugin-vue-jsx@5.1.5':
|
||||
resolution: {integrity: sha512-jIAsvHOEtWpslLOI2MeElGFxH7M8pM83BU/Tor4RLyiwH0FM4nUW3xdvbw20EeU9wc5IspQwMq225K3CMnJEpA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -7997,6 +8128,11 @@ packages:
|
||||
react-devtools-core@6.1.5:
|
||||
resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==}
|
||||
|
||||
react-dom@18.3.1:
|
||||
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
|
||||
react-dom@19.1.0:
|
||||
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
|
||||
peerDependencies:
|
||||
@ -8167,6 +8303,10 @@ packages:
|
||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
react-refresh@0.17.0:
|
||||
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
react-remove-scroll-bar@2.3.8:
|
||||
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
||||
engines: {node: '>=10'}
|
||||
@ -8197,6 +8337,10 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react@18.3.1:
|
||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
react@19.1.0:
|
||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -8412,6 +8556,9 @@ packages:
|
||||
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
|
||||
engines: {node: '>=11.0.0'}
|
||||
|
||||
scheduler@0.23.2:
|
||||
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
|
||||
|
||||
scheduler@0.26.0:
|
||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
||||
|
||||
@ -9430,6 +9577,46 @@ packages:
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
vite@6.4.3:
|
||||
resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||
jiti: '>=1.21.0'
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
sass-embedded: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.16.0
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
jiti:
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vite@7.3.3:
|
||||
resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -10947,7 +11134,7 @@ snapshots:
|
||||
wrap-ansi: 7.0.0
|
||||
ws: 8.20.0
|
||||
optionalDependencies:
|
||||
expo-router: 6.0.23(@expo/metro-runtime@6.1.2)(@types/react@19.2.14)(expo-constants@18.0.13)(expo-linking@8.0.12)(expo@54.0.34)(react-dom@19.1.0(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)
|
||||
expo-router: 6.0.23(@expo/metro-runtime@6.1.2)(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(expo-constants@18.0.13)(expo-linking@8.0.12)(expo@54.0.34)(react-dom@19.1.0(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)
|
||||
react-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@ -11221,6 +11408,8 @@ snapshots:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@fontsource/nunito@5.2.7': {}
|
||||
|
||||
'@hono/node-server@1.19.11(hono@4.12.17)':
|
||||
dependencies:
|
||||
hono: 4.12.17
|
||||
@ -12643,11 +12832,11 @@ snapshots:
|
||||
|
||||
'@prisma/client-runtime-utils@7.8.0': {}
|
||||
|
||||
'@prisma/client@7.8.0(prisma@7.8.0(@types/react@19.2.14)(magicast@0.5.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3))(typescript@5.9.3)':
|
||||
'@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(magicast@0.5.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@prisma/client-runtime-utils': 7.8.0
|
||||
optionalDependencies:
|
||||
prisma: 7.8.0(@types/react@19.2.14)(magicast@0.5.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3)
|
||||
prisma: 7.8.0(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(magicast@0.5.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
|
||||
'@prisma/config@7.8.0(magicast@0.5.2)':
|
||||
@ -12721,9 +12910,9 @@ snapshots:
|
||||
env-paths: 3.0.0
|
||||
proper-lockfile: 4.1.2
|
||||
|
||||
'@prisma/studio-core@0.27.3(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@prisma/studio-core@0.27.3(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-toggle': 1.1.10(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-toggle': 1.1.10(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@types/react': 19.2.14
|
||||
chart.js: 4.5.1
|
||||
react: 19.1.0
|
||||
@ -12739,16 +12928,17 @@ snapshots:
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
|
||||
'@radix-ui/react-collection@1.1.7(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 18.3.7(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.1.0)':
|
||||
dependencies:
|
||||
@ -12762,18 +12952,18 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-dialog@1.1.15(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-focus-scope': 1.1.7(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-portal': 1.1.9(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-presence': 1.1.5(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0)
|
||||
aria-hidden: 1.2.6
|
||||
@ -12782,6 +12972,7 @@ snapshots:
|
||||
react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 18.3.7(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.1.0)':
|
||||
dependencies:
|
||||
@ -12789,17 +12980,18 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.1.11(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 18.3.7(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.1.0)':
|
||||
dependencies:
|
||||
@ -12807,15 +12999,16 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-focus-scope@1.1.7(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 18.3.7(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.1.0)':
|
||||
dependencies:
|
||||
@ -12824,16 +13017,17 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-portal@1.1.9(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 18.3.7(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-presence@1.1.5(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0)
|
||||
@ -12841,30 +13035,33 @@ snapshots:
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 18.3.7(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-primitive@2.1.3(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 18.3.7(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.11(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-collection': 1.1.7(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 18.3.7(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-slot@1.2.0(@types/react@19.2.14)(react@19.1.0)':
|
||||
dependencies:
|
||||
@ -12880,30 +13077,32 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-tabs@1.1.13(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-presence': 1.1.5(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-roving-focus': 1.1.11(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 18.3.7(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-toggle@1.1.10(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@radix-ui/react-toggle@1.1.10(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 18.3.7(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.1.0)':
|
||||
dependencies:
|
||||
@ -13150,6 +13349,8 @@ snapshots:
|
||||
|
||||
'@rolldown/pluginutils@1.0.0': {}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-rc.13': {}
|
||||
|
||||
'@rollup/plugin-alias@6.0.0(rollup@4.60.3)':
|
||||
@ -13473,6 +13674,55 @@ snapshots:
|
||||
'@tanstack/virtual-core': 3.14.0
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
'@tauri-apps/api@2.11.0': {}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@2.11.2':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-darwin-x64@2.11.2':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf@2.11.2':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-gnu@2.11.2':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.11.2':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-riscv64-gnu@2.11.2':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.11.2':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@2.11.2':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.11.2':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-win32-ia32-msvc@2.11.2':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-win32-x64-msvc@2.11.2':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli@2.11.2':
|
||||
optionalDependencies:
|
||||
'@tauri-apps/cli-darwin-arm64': 2.11.2
|
||||
'@tauri-apps/cli-darwin-x64': 2.11.2
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf': 2.11.2
|
||||
'@tauri-apps/cli-linux-arm64-gnu': 2.11.2
|
||||
'@tauri-apps/cli-linux-arm64-musl': 2.11.2
|
||||
'@tauri-apps/cli-linux-riscv64-gnu': 2.11.2
|
||||
'@tauri-apps/cli-linux-x64-gnu': 2.11.2
|
||||
'@tauri-apps/cli-linux-x64-musl': 2.11.2
|
||||
'@tauri-apps/cli-win32-arm64-msvc': 2.11.2
|
||||
'@tauri-apps/cli-win32-ia32-msvc': 2.11.2
|
||||
'@tauri-apps/cli-win32-x64-msvc': 2.11.2
|
||||
|
||||
'@tiptap/core@3.23.1(@tiptap/pm@3.23.1)':
|
||||
dependencies:
|
||||
'@tiptap/pm': 3.23.1
|
||||
@ -13769,6 +14019,22 @@ snapshots:
|
||||
pg-protocol: 1.13.0
|
||||
pg-types: 2.2.0
|
||||
|
||||
'@types/prop-types@15.7.15': {}
|
||||
|
||||
'@types/react-dom@18.3.7(@types/react@18.3.31)':
|
||||
dependencies:
|
||||
'@types/react': 18.3.31
|
||||
|
||||
'@types/react-dom@18.3.7(@types/react@19.2.14)':
|
||||
dependencies:
|
||||
'@types/react': 19.2.14
|
||||
optional: true
|
||||
|
||||
'@types/react@18.3.31':
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.15
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/react@19.2.14':
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
@ -13987,6 +14253,18 @@ snapshots:
|
||||
- uploadthing
|
||||
- utf-8-validate
|
||||
|
||||
'@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
|
||||
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
|
||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.17.0
|
||||
vite: 6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
@ -15943,12 +16221,12 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
expo-router@6.0.23(@expo/metro-runtime@6.1.2)(@types/react@19.2.14)(expo-constants@18.0.13)(expo-linking@8.0.12)(expo@54.0.34)(react-dom@19.1.0(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0):
|
||||
expo-router@6.0.23(@expo/metro-runtime@6.1.2)(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(expo-constants@18.0.13)(expo-linking@8.0.12)(expo@54.0.34)(react-dom@19.1.0(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@expo/metro-runtime': 6.1.2(expo@54.0.34)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)
|
||||
'@expo/schema-utils': 0.1.8
|
||||
'@radix-ui/react-slot': 1.2.0(@types/react@19.2.14)(react@19.1.0)
|
||||
'@radix-ui/react-tabs': 1.1.13(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-tabs': 1.1.13(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@react-navigation/bottom-tabs': 7.15.11(@react-navigation/native@7.2.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)
|
||||
'@react-navigation/native': 7.2.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)
|
||||
'@react-navigation/native-stack': 7.14.12(@react-navigation/native@7.2.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)
|
||||
@ -15974,7 +16252,7 @@ snapshots:
|
||||
sf-symbols-typescript: 2.2.0
|
||||
shallowequal: 1.1.0
|
||||
use-latest-callback: 0.2.6(react@19.1.0)
|
||||
vaul: 1.1.2(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
vaul: 1.1.2(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
optionalDependencies:
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)
|
||||
@ -18648,12 +18926,12 @@ snapshots:
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 18.3.1
|
||||
|
||||
prisma@7.8.0(@types/react@19.2.14)(magicast@0.5.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3):
|
||||
prisma@7.8.0(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(magicast@0.5.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@prisma/config': 7.8.0(magicast@0.5.2)
|
||||
'@prisma/dev': 0.24.3(typescript@5.9.3)
|
||||
'@prisma/engines': 7.8.0
|
||||
'@prisma/studio-core': 0.27.3(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@prisma/studio-core': 0.27.3(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
mysql2: 3.15.3
|
||||
postgres: 3.4.7
|
||||
optionalDependencies:
|
||||
@ -18837,6 +19115,12 @@ snapshots:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
react-dom@18.3.1(react@18.3.1):
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
|
||||
react-dom@19.1.0(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@ -19049,6 +19333,8 @@ snapshots:
|
||||
|
||||
react-refresh@0.14.2: {}
|
||||
|
||||
react-refresh@0.17.0: {}
|
||||
|
||||
react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@ -19076,6 +19362,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
react@18.3.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
react@19.1.0: {}
|
||||
|
||||
read-cache@1.0.0:
|
||||
@ -19307,6 +19597,10 @@ snapshots:
|
||||
|
||||
sax@1.6.0: {}
|
||||
|
||||
scheduler@0.23.2:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
scheduler@0.26.0: {}
|
||||
|
||||
scule@1.3.0: {}
|
||||
@ -20274,9 +20568,9 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
vaul@1.1.2(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
vaul@1.1.2(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@radix-ui/react-dialog': 1.1.15(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
transitivePeerDependencies:
|
||||
@ -20323,7 +20617,7 @@ snapshots:
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
vite: 6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
@ -20415,7 +20709,7 @@ snapshots:
|
||||
vite@5.4.21(@types/node@22.19.17)(lightningcss@1.32.0)(terser@5.46.2):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.14
|
||||
postcss: 8.5.15
|
||||
rollup: 4.60.3
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.17
|
||||
@ -20423,12 +20717,28 @@ snapshots:
|
||||
lightningcss: 1.32.0
|
||||
terser: 5.46.2
|
||||
|
||||
vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.15
|
||||
rollup: 4.60.3
|
||||
tinyglobby: 0.2.16
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.17
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.7.0
|
||||
lightningcss: 1.32.0
|
||||
terser: 5.46.2
|
||||
yaml: 2.8.4
|
||||
|
||||
vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4):
|
||||
dependencies:
|
||||
esbuild: 0.27.7
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.14
|
||||
postcss: 8.5.15
|
||||
rollup: 4.60.3
|
||||
tinyglobby: 0.2.16
|
||||
optionalDependencies:
|
||||
|
||||