feat(magic): identify current device via hardwareId, migrate existing devices
This commit is contained in:
parent
e4b28be5be
commit
5b0a4d03d2
@ -36,7 +36,7 @@ function normalizeHostname(value: string): string {
|
|||||||
function mapToComputedDevice(d: MagicDeviceInfo, isCurrent: boolean): ComputedDevice {
|
function mapToComputedDevice(d: MagicDeviceInfo, isCurrent: boolean): ComputedDevice {
|
||||||
return {
|
return {
|
||||||
deviceId: d.deviceId,
|
deviceId: d.deviceId,
|
||||||
name: d.model ?? d.hostname,
|
name: d.hostname ?? d.model ?? "Unbenanntes Gerät",
|
||||||
platform: normalizePlatform(d.model ?? d.hostname),
|
platform: normalizePlatform(d.model ?? d.hostname),
|
||||||
model: d.model,
|
model: d.model,
|
||||||
osVersion: d.osVersion,
|
osVersion: d.osVersion,
|
||||||
@ -53,14 +53,16 @@ export function useDeviceStatus(
|
|||||||
localHostname: Ref<string | null>,
|
localHostname: Ref<string | null>,
|
||||||
iphone: Ref<IphoneDeviceState | null>,
|
iphone: Ref<IphoneDeviceState | null>,
|
||||||
currentDeviceId?: Ref<string | null>,
|
currentDeviceId?: Ref<string | null>,
|
||||||
|
currentHardwareId?: Ref<string | null>,
|
||||||
) {
|
) {
|
||||||
function isCurrentDevice(d: MagicDeviceInfo): boolean {
|
function isCurrentDevice(d: MagicDeviceInfo): boolean {
|
||||||
|
if (currentHardwareId?.value && d.hardwareId) {
|
||||||
|
return d.hardwareId === currentHardwareId.value;
|
||||||
|
}
|
||||||
if (currentDeviceId?.value) {
|
if (currentDeviceId?.value) {
|
||||||
return d.deviceId === currentDeviceId.value;
|
return d.deviceId === currentDeviceId.value;
|
||||||
}
|
}
|
||||||
if (!localHostname.value) return false;
|
return false;
|
||||||
const local = normalizeHostname(localHostname.value);
|
|
||||||
return normalizeHostname(d.hostname) === local;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBackendDevice = computed<ComputedDevice | null>(() => {
|
const currentBackendDevice = computed<ComputedDevice | null>(() => {
|
||||||
|
|||||||
32
apps/rebreak-magic/app/composables/useMagicState.ts
Normal file
32
apps/rebreak-magic/app/composables/useMagicState.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type { MagicDeviceInfo, RegisterDeviceResponse, IphoneDeviceState } from "./useTauri";
|
||||||
|
|
||||||
|
export interface MagicSessionState {
|
||||||
|
token?: string;
|
||||||
|
deviceId: string;
|
||||||
|
hardwareId?: string;
|
||||||
|
dnsToken: string;
|
||||||
|
profileUrl: string;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMagicSession() {
|
||||||
|
return useState<MagicSessionState | null>("magic-session", () => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMagicDevices() {
|
||||||
|
return useState<MagicDeviceInfo[]>("magic-devices", () => []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCurrentMagicDevice() {
|
||||||
|
const session = useMagicSession();
|
||||||
|
const devices = useMagicDevices();
|
||||||
|
|
||||||
|
return computed(() => {
|
||||||
|
if (!session.value) return null;
|
||||||
|
return devices.value.find((d) => d.deviceId === session.value!.deviceId) ?? null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIphoneDevice() {
|
||||||
|
return useState<IphoneDeviceState | null>("iphone-device", () => null);
|
||||||
|
}
|
||||||
@ -43,9 +43,15 @@ export function useProtectionStatus() {
|
|||||||
return iphone.value.isSupervised && hasEnrollment && hasLock;
|
return iphone.value.isSupervised && hasEnrollment && hasLock;
|
||||||
});
|
});
|
||||||
const currentBackendDevice = computed<MagicDeviceInfo | null>(() => {
|
const currentBackendDevice = computed<MagicDeviceInfo | null>(() => {
|
||||||
if (!localHostname.value) return null;
|
if (!session.value) return null;
|
||||||
const local = normalizeHostname(localHostname.value);
|
const hardwareId = session.value.hardwareId;
|
||||||
return devices.value.find((d) => normalizeHostname(d.hostname) === local) ?? null;
|
const deviceId = session.value.deviceId;
|
||||||
|
return (
|
||||||
|
devices.value.find((d) => {
|
||||||
|
if (hardwareId && d.hardwareId) return d.hardwareId === hardwareId;
|
||||||
|
return d.deviceId === deviceId;
|
||||||
|
}) ?? null
|
||||||
|
);
|
||||||
});
|
});
|
||||||
const desktopProtected = computed(() => !!desktopProtection.value?.active || !!currentBackendDevice.value);
|
const desktopProtected = computed(() => !!desktopProtection.value?.active || !!currentBackendDevice.value);
|
||||||
const anyBackendDevice = computed(() => devices.value.length > 0);
|
const anyBackendDevice = computed(() => devices.value.length > 0);
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export interface RegisterDeviceResponse {
|
|||||||
export interface MagicDeviceInfo {
|
export interface MagicDeviceInfo {
|
||||||
source: string;
|
source: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
|
hardwareId: string | null;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
model: string | null;
|
model: string | null;
|
||||||
osVersion: string | null;
|
osVersion: string | null;
|
||||||
@ -131,11 +132,18 @@ export function useTauri() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function registerDevice(
|
async function registerDevice(
|
||||||
deviceId: string,
|
model?: string,
|
||||||
model: string | undefined,
|
osVersion?: string,
|
||||||
osVersion: string | undefined,
|
|
||||||
): Promise<RegisterDeviceResponse> {
|
): Promise<RegisterDeviceResponse> {
|
||||||
return await invokeLogged("register_device", { deviceId, model, osVersion });
|
return await invokeLogged("register_device", { model, osVersion });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getHardwareId(): Promise<string> {
|
||||||
|
return await invokeLogged("get_hardware_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDeviceId(): Promise<string | null> {
|
||||||
|
return await invokeLogged("get_device_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getStoredSession(): Promise<MagicSession | null> {
|
async function getStoredSession(): Promise<MagicSession | null> {
|
||||||
@ -287,5 +295,7 @@ export function useTauri() {
|
|||||||
getDesktopProtectionStatus,
|
getDesktopProtectionStatus,
|
||||||
setDesktopProtectionStatus,
|
setDesktopProtectionStatus,
|
||||||
getHostname,
|
getHostname,
|
||||||
|
getHardwareId,
|
||||||
|
getDeviceId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -149,6 +149,9 @@ const {
|
|||||||
fetchMe,
|
fetchMe,
|
||||||
setDesktopProtectionStatus,
|
setDesktopProtectionStatus,
|
||||||
getPlatform,
|
getPlatform,
|
||||||
|
getHardwareId,
|
||||||
|
getDeviceId,
|
||||||
|
registerDevice,
|
||||||
} = useTauri();
|
} = useTauri();
|
||||||
|
|
||||||
const session = useMagicSession();
|
const session = useMagicSession();
|
||||||
@ -156,7 +159,8 @@ const devices = useMagicDevices();
|
|||||||
const iphone = useIphoneDevice();
|
const iphone = useIphoneDevice();
|
||||||
const protection = useProtectionStatus();
|
const protection = useProtectionStatus();
|
||||||
|
|
||||||
const currentDeviceId = computed(() => session.value?.deviceId ?? null);
|
const currentDeviceId = ref<string | null>(null);
|
||||||
|
const currentHardwareId = ref<string | null>(null);
|
||||||
|
|
||||||
const profile = ref<UserProfile | null>(null);
|
const profile = ref<UserProfile | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@ -172,7 +176,7 @@ const subscriptionInGracePeriod = ref(false);
|
|||||||
// Share localHostname from protection composable with device status logic.
|
// Share localHostname from protection composable with device status logic.
|
||||||
const localHostname = protection.localHostname;
|
const localHostname = protection.localHostname;
|
||||||
const { currentBackendDevice, iosDevices, desktopDevices, iosStars } =
|
const { currentBackendDevice, iosDevices, desktopDevices, iosStars } =
|
||||||
useDeviceStatus(devices, localHostname, iphone, currentDeviceId);
|
useDeviceStatus(devices, localHostname, iphone, currentDeviceId, currentHardwareId);
|
||||||
|
|
||||||
const selectedDeviceStars = computed(() => {
|
const selectedDeviceStars = computed(() => {
|
||||||
if (!selectedDevice.value || selectedDevice.value.platform !== "ios") return null;
|
if (!selectedDevice.value || selectedDevice.value.platform !== "ios") return null;
|
||||||
@ -193,8 +197,33 @@ onMounted(async () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
platformInfo.value = null;
|
platformInfo.value = null;
|
||||||
}
|
}
|
||||||
|
await initCurrentDevice();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function initCurrentDevice() {
|
||||||
|
try {
|
||||||
|
const hardwareId = await getHardwareId();
|
||||||
|
currentHardwareId.value = hardwareId;
|
||||||
|
|
||||||
|
// Ensure the backend knows this hardware ID.
|
||||||
|
// For existing devices this performs the one-time hardwareId migration.
|
||||||
|
const response = await registerDevice(
|
||||||
|
platformInfo.value?.platform,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
currentDeviceId.value = response.deviceId;
|
||||||
|
|
||||||
|
session.value = {
|
||||||
|
deviceId: response.deviceId,
|
||||||
|
hardwareId,
|
||||||
|
dnsToken: response.dnsToken,
|
||||||
|
profileUrl: response.profileUrl,
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Failed to initialize current device:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadProfile() {
|
async function loadProfile() {
|
||||||
try {
|
try {
|
||||||
profile.value = await fetchMe();
|
profile.value = await fetchMe();
|
||||||
|
|||||||
@ -21,8 +21,10 @@ pub struct RedeemPairingResponse {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RegisterDeviceRequest {
|
pub struct RegisterDeviceRequest {
|
||||||
#[serde(rename = "deviceId")]
|
#[serde(rename = "deviceId", skip_serializing_if = "Option::is_none")]
|
||||||
pub device_id: String,
|
pub device_id: Option<String>,
|
||||||
|
#[serde(rename = "hardwareId", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub hardware_id: Option<String>,
|
||||||
pub hostname: String,
|
pub hostname: String,
|
||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
#[serde(rename = "osVersion")]
|
#[serde(rename = "osVersion")]
|
||||||
@ -45,6 +47,8 @@ pub struct MagicDeviceInfo {
|
|||||||
pub source: String,
|
pub source: String,
|
||||||
#[serde(rename = "deviceId")]
|
#[serde(rename = "deviceId")]
|
||||||
pub device_id: String,
|
pub device_id: String,
|
||||||
|
#[serde(rename = "hardwareId")]
|
||||||
|
pub hardware_id: Option<String>,
|
||||||
pub hostname: String,
|
pub hostname: String,
|
||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
#[serde(rename = "osVersion")]
|
#[serde(rename = "osVersion")]
|
||||||
@ -159,14 +163,16 @@ impl MagicApiClient {
|
|||||||
pub async fn register_device(
|
pub async fn register_device(
|
||||||
&self,
|
&self,
|
||||||
token: &str,
|
token: &str,
|
||||||
device_id: &str,
|
device_id: Option<&str>,
|
||||||
|
hardware_id: Option<&str>,
|
||||||
hostname: &str,
|
hostname: &str,
|
||||||
model: Option<&str>,
|
model: Option<&str>,
|
||||||
os_version: Option<&str>,
|
os_version: Option<&str>,
|
||||||
) -> AppResult<RegisterDeviceResponse> {
|
) -> AppResult<RegisterDeviceResponse> {
|
||||||
let url = format!("{}/api/magic/register", self.base_url);
|
let url = format!("{}/api/magic/register", self.base_url);
|
||||||
let body = RegisterDeviceRequest {
|
let body = RegisterDeviceRequest {
|
||||||
device_id: device_id.to_string(),
|
device_id: device_id.map(|s| s.to_string()),
|
||||||
|
hardware_id: hardware_id.map(|s| s.to_string()),
|
||||||
hostname: hostname.to_string(),
|
hostname: hostname.to_string(),
|
||||||
model: model.map(|s| s.to_string()),
|
model: model.map(|s| s.to_string()),
|
||||||
os_version: os_version.map(|s| s.to_string()),
|
os_version: os_version.map(|s| s.to_string()),
|
||||||
|
|||||||
223
apps/rebreak-magic/src-tauri/src/config.rs
Normal file
223
apps/rebreak-magic/src-tauri/src/config.rs
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use crate::error::{AppError, AppResult};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub backend_base_url: String,
|
||||||
|
pub mdm_server_url: String,
|
||||||
|
pub supabase_url: String,
|
||||||
|
pub supabase_anon_key: String,
|
||||||
|
/// Stable device identity known to the backend (UserDevice.deviceId).
|
||||||
|
/// Filled after first successful device registration.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub device_id: Option<String>,
|
||||||
|
/// Hardware-bound identity (UserDevice.hardwareId).
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub hardware_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
backend_base_url: "https://staging.rebreak.org".to_string(),
|
||||||
|
mdm_server_url: "https://mdm.rebreak.org".to_string(),
|
||||||
|
supabase_url: String::new(),
|
||||||
|
supabase_anon_key: String::new(),
|
||||||
|
device_id: None,
|
||||||
|
hardware_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYRING_SERVICE: &str = "org.rebreak.magic";
|
||||||
|
const KEYRING_ACCOUNT: &str = "magic-session";
|
||||||
|
const SESSION_FILE: &str = "session.json";
|
||||||
|
const DESKTOP_PROTECTION_FILE: &str = "desktop-protection.json";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MagicSession {
|
||||||
|
pub access_token: String,
|
||||||
|
pub session_id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
#[serde(with = "chrono::serde::ts_seconds")]
|
||||||
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub label: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MagicSession {
|
||||||
|
pub fn new(access_token: String, session_id: String, label: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
access_token,
|
||||||
|
session_id,
|
||||||
|
user_id: String::new(),
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
label,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BinderConfig {
|
||||||
|
#[serde(rename = "mdmServer")]
|
||||||
|
pub mdm_server: String,
|
||||||
|
#[serde(rename = "mdmUser")]
|
||||||
|
pub mdm_user: String,
|
||||||
|
#[serde(rename = "mdmApiKey")]
|
||||||
|
pub mdm_api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DesktopProtectionState {
|
||||||
|
pub active: bool,
|
||||||
|
pub platform: String,
|
||||||
|
#[serde(with = "chrono::serde::ts_seconds")]
|
||||||
|
pub activated_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfig {
|
||||||
|
pub fn config_dir() -> AppResult<PathBuf> {
|
||||||
|
let dir = dirs::config_dir()
|
||||||
|
.ok_or_else(|| AppError::new("Could not find config directory"))?
|
||||||
|
.join("org.rebreak.magic");
|
||||||
|
std::fs::create_dir_all(&dir)?;
|
||||||
|
Ok(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config_path() -> AppResult<PathBuf> {
|
||||||
|
Ok(Self::config_dir()?.join("config.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binder_config_path() -> AppResult<PathBuf> {
|
||||||
|
Ok(dirs::config_dir()
|
||||||
|
.ok_or_else(|| AppError::new("Could not find config directory"))?
|
||||||
|
.join("rebreak-binder")
|
||||||
|
.join("config.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_binder_config() -> AppResult<BinderConfig> {
|
||||||
|
let path = Self::binder_config_path()?;
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(AppError::new(
|
||||||
|
"rebreak-binder config nicht gefunden. Bitte README → 'Config (lokal)'.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let json = std::fs::read_to_string(&path)?;
|
||||||
|
serde_json::from_str(&json)
|
||||||
|
.map_err(|e| AppError::new(format!("rebreak-binder config kaputt: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load() -> Self {
|
||||||
|
match Self::config_path() {
|
||||||
|
Ok(path) if path.exists() => {
|
||||||
|
std::fs::read_to_string(&path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
_ => Self::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self) -> AppResult<()> {
|
||||||
|
let path = Self::config_path()?;
|
||||||
|
let contents = serde_json::to_string_pretty(self)?;
|
||||||
|
std::fs::write(path, contents)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_path() -> AppResult<PathBuf> {
|
||||||
|
Ok(Self::config_dir()?.join(SESSION_FILE))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_magic_session(session: &MagicSession) -> AppResult<()> {
|
||||||
|
let path = Self::session_path()?;
|
||||||
|
let json = serde_json::to_string_pretty(session)
|
||||||
|
.map_err(|e| AppError::new(format!("Failed to serialize session: {}", e)))?;
|
||||||
|
std::fs::write(&path, json)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_magic_session() -> AppResult<Option<MagicSession>> {
|
||||||
|
let path = match Self::session_path() {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = std::fs::read_to_string(&path)?;
|
||||||
|
if json.trim().is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = serde_json::from_str(&json)
|
||||||
|
.map_err(|e| AppError::new(format!("Failed to parse session: {}", e)))?;
|
||||||
|
Ok(Some(session))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_magic_session() -> AppResult<()> {
|
||||||
|
let path = match Self::session_path() {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return Ok(()),
|
||||||
|
};
|
||||||
|
if path.exists() {
|
||||||
|
std::fs::remove_file(&path)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desktop_protection_path() -> AppResult<PathBuf> {
|
||||||
|
Ok(Self::config_dir()?.join(DESKTOP_PROTECTION_FILE))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_desktop_protection(state: &DesktopProtectionState) -> AppResult<()> {
|
||||||
|
let path = Self::desktop_protection_path()?;
|
||||||
|
let json = serde_json::to_string_pretty(state)
|
||||||
|
.map_err(|e| AppError::new(format!("Failed to serialize desktop protection state: {}", e)))?;
|
||||||
|
std::fs::write(&path, json)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_desktop_protection() -> AppResult<Option<DesktopProtectionState>> {
|
||||||
|
let path = match Self::desktop_protection_path() {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return Ok(None),
|
||||||
|
};
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let json = std::fs::read_to_string(&path)?;
|
||||||
|
if json.trim().is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let state = serde_json::from_str(&json)
|
||||||
|
.map_err(|e| AppError::new(format!("Failed to parse desktop protection state: {}", e)))?;
|
||||||
|
Ok(Some(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_desktop_protection() -> AppResult<()> {
|
||||||
|
let path = match Self::desktop_protection_path() {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return Ok(()),
|
||||||
|
};
|
||||||
|
if path.exists() {
|
||||||
|
std::fs::remove_file(&path)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_device_id(&mut self, id: String) -> AppResult<()> {
|
||||||
|
self.device_id = Some(id);
|
||||||
|
self.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_hardware_id(&mut self, id: String) -> AppResult<()> {
|
||||||
|
self.hardware_id = Some(id);
|
||||||
|
self.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -54,6 +54,8 @@ pub fn run() {
|
|||||||
get_desktop_protection_status,
|
get_desktop_protection_status,
|
||||||
set_desktop_protection_status,
|
set_desktop_protection_status,
|
||||||
get_hostname,
|
get_hostname,
|
||||||
|
get_hardware_id,
|
||||||
|
get_device_id,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
@ -71,6 +73,26 @@ fn hostname() -> AppResult<String> {
|
|||||||
Ok(hostname)
|
Ok(hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_or_init_hardware_id(config: &mut AppConfig) -> AppResult<String> {
|
||||||
|
if let Some(id) = config.hardware_id.clone() {
|
||||||
|
return Ok(id);
|
||||||
|
}
|
||||||
|
let id = platform::get_hardware_id()?;
|
||||||
|
config.set_hardware_id(id.clone())?;
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_hardware_id() -> AppResult<String> {
|
||||||
|
let mut config = AppConfig::load();
|
||||||
|
get_or_init_hardware_id(&mut config)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_device_id() -> AppResult<Option<String>> {
|
||||||
|
Ok(AppConfig::load().device_id)
|
||||||
|
}
|
||||||
|
|
||||||
fn require_session() -> AppResult<MagicSession> {
|
fn require_session() -> AppResult<MagicSession> {
|
||||||
AppConfig::load_magic_session()?.ok_or_else(|| {
|
AppConfig::load_magic_session()?.ok_or_else(|| {
|
||||||
error::AppError::new("Keine Magic-Session gefunden. Bitte neu paaren.")
|
error::AppError::new("Keine Magic-Session gefunden. Bitte neu paaren.")
|
||||||
@ -99,17 +121,28 @@ async fn redeem_pairing_code(code: String, label: Option<String>) -> AppResult<R
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn register_device(
|
async fn register_device(
|
||||||
device_id: String,
|
|
||||||
model: Option<String>,
|
model: Option<String>,
|
||||||
os_version: Option<String>,
|
os_version: Option<String>,
|
||||||
) -> AppResult<RegisterDeviceResponse> {
|
) -> AppResult<RegisterDeviceResponse> {
|
||||||
let session = require_session()?;
|
let session = require_session()?;
|
||||||
let config = AppConfig::load();
|
let mut config = AppConfig::load();
|
||||||
let client = MagicApiClient::new(&config);
|
let client = MagicApiClient::new(&config);
|
||||||
let hostname = hostname()?;
|
let hostname = hostname()?;
|
||||||
|
|
||||||
|
// Hardware-ID ist der stabile Identifikator; deviceId wird vom Backend zurückgeliefert.
|
||||||
|
let hardware_id = get_or_init_hardware_id(&mut config)?;
|
||||||
let response = client
|
let response = client
|
||||||
.register_device(&session.access_token, &device_id, &hostname, model.as_deref(), os_version.as_deref())
|
.register_device(
|
||||||
|
&session.access_token,
|
||||||
|
config.device_id.as_deref(),
|
||||||
|
Some(&hardware_id),
|
||||||
|
&hostname,
|
||||||
|
model.as_deref(),
|
||||||
|
os_version.as_deref(),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
config.set_device_id(response.device_id.clone())?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
74
apps/rebreak-magic/src-tauri/src/platform/macos.rs
Normal file
74
apps/rebreak-magic/src-tauri/src/platform/macos.rs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use super::{Platform, PlatformInfo};
|
||||||
|
use crate::error::AppResult;
|
||||||
|
|
||||||
|
pub fn get_platform_info() -> AppResult<PlatformInfo> {
|
||||||
|
Ok(PlatformInfo {
|
||||||
|
platform: Platform::MacOS,
|
||||||
|
version: "14.0".to_string(), // TODO: Read actual macOS version
|
||||||
|
supports_ios_supervision: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_dns_profile(profile_path: &str) -> AppResult<()> {
|
||||||
|
use std::process::Command;
|
||||||
|
let status = Command::new("open")
|
||||||
|
.arg(profile_path)
|
||||||
|
.status()
|
||||||
|
.map_err(|e| crate::error::AppError::new(format!("Failed to open profile: {}", e)))?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
return Err(crate::error::AppError::new(
|
||||||
|
"System Settings konnte das Profil nicht öffnen".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store_token(_token: &str) -> AppResult<()> {
|
||||||
|
// TODO: Implement using macOS Keychain via keyring crate
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_token() -> AppResult<Option<String>> {
|
||||||
|
// TODO: Implement using macOS Keychain via keyring crate
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_protection(profile_path: &str) -> AppResult<()> {
|
||||||
|
install_dns_profile(profile_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deactivate_protection(_token: &str) -> AppResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the macOS system UUID (IOPlatformUUID).
|
||||||
|
pub fn get_hardware_id() -> AppResult<String> {
|
||||||
|
use std::process::Command;
|
||||||
|
let output = Command::new("ioreg")
|
||||||
|
.args([
|
||||||
|
"-rd1",
|
||||||
|
"-c",
|
||||||
|
"IOPlatformExpertDevice",
|
||||||
|
"-k",
|
||||||
|
"IOPlatformUUID",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.map_err(|e| crate::error::AppError::new(format!("Failed to read hardware ID: {}", e)))?;
|
||||||
|
|
||||||
|
let text = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in text.lines() {
|
||||||
|
if line.contains("IOPlatformUUID") {
|
||||||
|
let parts: Vec<&str> = line.split('"').collect();
|
||||||
|
if let Some(uuid) = parts.iter().rev().find(|s| s.contains('-')) {
|
||||||
|
return Ok(uuid.trim().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(crate::error::AppError::new(
|
||||||
|
"Could not parse macOS hardware UUID".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
75
apps/rebreak-magic/src-tauri/src/platform/mod.rs
Normal file
75
apps/rebreak-magic/src-tauri/src/platform/mod.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use crate::error::AppResult;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macos;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod windows;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum Platform {
|
||||||
|
MacOS,
|
||||||
|
Windows,
|
||||||
|
Linux,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PlatformInfo {
|
||||||
|
pub platform: Platform,
|
||||||
|
pub version: String,
|
||||||
|
pub supports_ios_supervision: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_platform() -> AppResult<PlatformInfo> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
return macos::get_platform_info();
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
return windows::get_platform_info();
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||||
|
Ok(PlatformInfo {
|
||||||
|
platform: Platform::Unknown,
|
||||||
|
version: String::new(),
|
||||||
|
supports_ios_supervision: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PlatformInterface {
|
||||||
|
fn install_dns_profile(&self, profile_path: &str) -> AppResult<()>;
|
||||||
|
fn store_token(&self, token: &str) -> AppResult<()>;
|
||||||
|
fn read_token(&self) -> AppResult<Option<String>>;
|
||||||
|
fn activate_protection(&self, token: &str) -> AppResult<()>;
|
||||||
|
fn deactivate_protection(&self, token: &str) -> AppResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_protection(profile_or_token: &str) -> AppResult<()> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
return macos::activate_protection(profile_or_token);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
return windows::activate_protection(profile_or_token);
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||||
|
Err(crate::error::AppError::new(
|
||||||
|
"Plattform wird nicht unterstützt".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_hardware_id() -> AppResult<String> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
return macos::get_hardware_id();
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
return windows::get_hardware_id();
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||||
|
Err(crate::error::AppError::new(
|
||||||
|
"Hardware-ID wird auf dieser Plattform nicht unterstützt".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
94
apps/rebreak-magic/src-tauri/src/platform/windows.rs
Normal file
94
apps/rebreak-magic/src-tauri/src/platform/windows.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use super::{Platform, PlatformInfo};
|
||||||
|
use crate::error::AppResult;
|
||||||
|
|
||||||
|
pub fn get_platform_info() -> AppResult<PlatformInfo> {
|
||||||
|
Ok(PlatformInfo {
|
||||||
|
platform: Platform::Windows,
|
||||||
|
version: "11".to_string(), // TODO: Read actual Windows version
|
||||||
|
supports_ios_supervision: true, // Once supervise-magic Windows build is verified
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_doh(dns_token: &str) -> AppResult<()> {
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let doh_url = format!("https://dns.rebreak.org/dns-query/{}", dns_token);
|
||||||
|
let script = format!(
|
||||||
|
r#"
|
||||||
|
$iface = Get-NetAdapter | Where-Object {{ $_.Status -eq 'Up' }} | Select-Object -First 1
|
||||||
|
if (-not $iface) {{ exit 1 }}
|
||||||
|
Set-DnsClientDohServerAddress -ServerAddress '142.132.245.42' -DohTemplate '{}' -AutoUpgrade $true
|
||||||
|
Set-DnsClientServerAddress -InterfaceAlias $iface.Name -ServerAddresses ('142.132.245.42')
|
||||||
|
"#,
|
||||||
|
doh_url
|
||||||
|
);
|
||||||
|
|
||||||
|
let status = Command::new("powershell.exe")
|
||||||
|
.args([
|
||||||
|
"-NonInteractive",
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-Command",
|
||||||
|
&script,
|
||||||
|
])
|
||||||
|
.status()
|
||||||
|
.map_err(|e| crate::error::AppError::new(format!("Failed to run PowerShell: {}", e)))?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
return Err(crate::error::AppError::new(
|
||||||
|
"DoH-Konfiguration fehlgeschlagen. Bitte ReBreak Magic mit Administratorrechten starten.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store_token(token: &str) -> AppResult<()> {
|
||||||
|
// TODO: Implement using Windows Credential Manager via keyring crate
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_token() -> AppResult<Option<String>> {
|
||||||
|
// TODO: Implement using Windows Credential Manager via keyring crate
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_protection(token: &str) -> AppResult<()> {
|
||||||
|
install_doh(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deactivate_protection(_token: &str) -> AppResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Windows machine GUID (Registry: HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid).
|
||||||
|
pub fn get_hardware_id() -> AppResult<String> {
|
||||||
|
use std::process::Command;
|
||||||
|
let output = Command::new("reg")
|
||||||
|
.args([
|
||||||
|
"query",
|
||||||
|
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography",
|
||||||
|
"/v",
|
||||||
|
"MachineGuid",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.map_err(|e| crate::error::AppError::new(format!("Failed to read hardware ID: {}", e)))?;
|
||||||
|
|
||||||
|
let text = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in text.lines() {
|
||||||
|
if line.contains("MachineGuid") {
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if let Some(uuid) = parts.last() {
|
||||||
|
if uuid.contains('-') {
|
||||||
|
return Ok(uuid.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(crate::error::AppError::new(
|
||||||
|
"Could not parse Windows machine GUID".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
-- Hardware-ID für UserDevices
|
||||||
|
-- Client liefert hardwaregebundene ID; Backend speichert sie separat zur bestehenden deviceId.
|
||||||
|
|
||||||
|
ALTER TABLE "rebreak"."user_devices"
|
||||||
|
ADD COLUMN IF NOT EXISTS "hardware_id" TEXT;
|
||||||
|
|
||||||
|
-- Eindeutigkeit pro User + Hardware-ID (NULL-Werte sind bei UNIQUE erlaubt)
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "user_devices_user_id_hardware_id_key"
|
||||||
|
ON "rebreak"."user_devices"("user_id", "hardware_id");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_devices_hardware_id_idx"
|
||||||
|
ON "rebreak"."user_devices"("hardware_id");
|
||||||
@ -1102,6 +1102,9 @@ model UserDevice {
|
|||||||
/// Wird in com.apple.profileRemovalPassword injiziert — User sieht es NIE,
|
/// Wird in com.apple.profileRemovalPassword injiziert — User sieht es NIE,
|
||||||
/// nur nach Cooldown-Release (Offboarding). NULL → noch keins generiert.
|
/// nur nach Cooldown-Release (Offboarding). NULL → noch keins generiert.
|
||||||
magicRemovalPassword String? @map("magic_removal_password")
|
magicRemovalPassword String? @map("magic_removal_password")
|
||||||
|
/// Hardware-gebundene ID (z. B. System-UUID, ANDROID_ID, IDFV).
|
||||||
|
/// Wird vom Client geliefert, nicht vom Backend generiert.
|
||||||
|
hardwareId String? @map("hardware_id")
|
||||||
/// Wann der User die Entfernung des Magic-Profils beantragt hat.
|
/// Wann der User die Entfernung des Magic-Profils beantragt hat.
|
||||||
/// Removal-Passwort wird erst nach +MAGIC_RELEASE_COOLDOWN_H sichtbar.
|
/// Removal-Passwort wird erst nach +MAGIC_RELEASE_COOLDOWN_H sichtbar.
|
||||||
magicReleaseRequestedAt DateTime? @map("magic_release_requested_at")
|
magicReleaseRequestedAt DateTime? @map("magic_release_requested_at")
|
||||||
@ -1109,8 +1112,10 @@ model UserDevice {
|
|||||||
magicCooldownUntil DateTime? @map("magic_cooldown_until")
|
magicCooldownUntil DateTime? @map("magic_cooldown_until")
|
||||||
|
|
||||||
@@unique([userId, deviceId])
|
@@unique([userId, deviceId])
|
||||||
|
@@unique([userId, hardwareId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([deviceId])
|
@@index([deviceId])
|
||||||
|
@@index([hardwareId])
|
||||||
@@map("user_devices")
|
@@map("user_devices")
|
||||||
@@schema("rebreak")
|
@@schema("rebreak")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
deviceId: true,
|
deviceId: true,
|
||||||
|
hardwareId: true,
|
||||||
platform: true,
|
platform: true,
|
||||||
model: true,
|
model: true,
|
||||||
name: true,
|
name: true,
|
||||||
@ -64,6 +65,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
return {
|
return {
|
||||||
source: "magic" as const,
|
source: "magic" as const,
|
||||||
deviceId: d.deviceId,
|
deviceId: d.deviceId,
|
||||||
|
hardwareId: d.hardwareId,
|
||||||
hostname: d.hostname ?? "Unbenanntes Ger\u00e4t",
|
hostname: d.hostname ?? "Unbenanntes Ger\u00e4t",
|
||||||
model: d.model,
|
model: d.model,
|
||||||
osVersion: d.osVersion,
|
osVersion: d.osVersion,
|
||||||
@ -86,6 +88,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
return {
|
return {
|
||||||
source: "locked" as const,
|
source: "locked" as const,
|
||||||
deviceId: d.deviceId,
|
deviceId: d.deviceId,
|
||||||
|
hardwareId: d.hardwareId,
|
||||||
hostname: d.name ?? d.model ?? prettyPlatform(d.platform),
|
hostname: d.name ?? d.model ?? prettyPlatform(d.platform),
|
||||||
model: d.model,
|
model: d.model,
|
||||||
osVersion: d.osVersion,
|
osVersion: d.osVersion,
|
||||||
|
|||||||
@ -22,21 +22,33 @@ import { generateRemovalPassword } from "../../utils/magic-lock";
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const user = await requireUser(event);
|
const user = await requireUser(event);
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const { deviceId, hostname, model, osVersion, platform } = body as {
|
const { deviceId, hardwareId, hostname, model, osVersion, platform } = body as {
|
||||||
deviceId?: string;
|
deviceId?: string;
|
||||||
|
hardwareId?: string;
|
||||||
hostname?: string;
|
hostname?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
osVersion?: string;
|
osVersion?: string;
|
||||||
platform?: string;
|
platform?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!deviceId || !hostname) {
|
if (!hostname) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: "deviceId und hostname required",
|
message: "hostname required",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!deviceId && !hardwareId) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "deviceId oder hardwareId required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für neue Magic-Registrierungen: hardwareId wird gleichzeitig deviceId,
|
||||||
|
// damit das Backend keine eigene ID generieren muss.
|
||||||
|
const effectiveDeviceId = deviceId?.trim() || hardwareId!.trim();
|
||||||
|
|
||||||
// Plattform: Mac-App sendet nichts (legacy default), Windows-App sendet "windows"
|
// Plattform: Mac-App sendet nichts (legacy default), Windows-App sendet "windows"
|
||||||
const devicePlatform =
|
const devicePlatform =
|
||||||
platform === "windows" ? "windows" : "macos";
|
platform === "windows" ? "windows" : "macos";
|
||||||
@ -44,17 +56,60 @@ export default defineEventHandler(async (event) => {
|
|||||||
const db = usePrisma();
|
const db = usePrisma();
|
||||||
|
|
||||||
// 1. Prüfe ob Device bereits als Magic-Client gebunden ist (idempotent)
|
// 1. Prüfe ob Device bereits als Magic-Client gebunden ist (idempotent)
|
||||||
const existing = await db.userDevice.findUnique({
|
// Priorität: hardwareId → deviceId → Migration über Modell/Plattform/OS.
|
||||||
where: { userId_deviceId: { userId: user.id, deviceId } },
|
let existing = null;
|
||||||
select: {
|
|
||||||
id: true,
|
if (hardwareId) {
|
||||||
userId: true,
|
existing = await db.userDevice.findFirst({
|
||||||
magicDnsToken: true,
|
where: { userId: user.id, hardwareId },
|
||||||
magicEnrolledAt: true,
|
select: {
|
||||||
magicRevokedAt: true,
|
id: true,
|
||||||
magicRemovalPassword: true,
|
userId: true,
|
||||||
},
|
deviceId: true,
|
||||||
});
|
magicDnsToken: true,
|
||||||
|
magicEnrolledAt: true,
|
||||||
|
magicRevokedAt: true,
|
||||||
|
magicRemovalPassword: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing && deviceId) {
|
||||||
|
existing = await db.userDevice.findUnique({
|
||||||
|
where: { userId_deviceId: { userId: user.id, deviceId } },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userId: true,
|
||||||
|
deviceId: true,
|
||||||
|
magicDnsToken: true,
|
||||||
|
magicEnrolledAt: true,
|
||||||
|
magicRevokedAt: true,
|
||||||
|
magicRemovalPassword: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration: bestehendes Gerät ohne hardwareId anhand von Modell/Plattform/OS finden.
|
||||||
|
if (!existing && hardwareId) {
|
||||||
|
existing = await db.userDevice.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
hardwareId: null,
|
||||||
|
platform: devicePlatform,
|
||||||
|
model: model ?? null,
|
||||||
|
osVersion: osVersion ?? null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userId: true,
|
||||||
|
deviceId: true,
|
||||||
|
magicDnsToken: true,
|
||||||
|
magicEnrolledAt: true,
|
||||||
|
magicRevokedAt: true,
|
||||||
|
magicRemovalPassword: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Wenn Token existiert und nicht revoked → return existing
|
// Wenn Token existiert und nicht revoked → return existing
|
||||||
if (
|
if (
|
||||||
@ -132,21 +187,26 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Upsert UserDevice (platform="macos" | "windows")
|
// 5. Upsert UserDevice (platform="macos" | "windows")
|
||||||
|
// Bei Migration behalten wir die bestehende deviceId bei.
|
||||||
|
const upsertDeviceId = existing?.deviceId || effectiveDeviceId;
|
||||||
|
|
||||||
const device = await db.userDevice.upsert({
|
const device = await db.userDevice.upsert({
|
||||||
where: { userId_deviceId: { userId: user.id, deviceId } },
|
where: { userId_deviceId: { userId: user.id, deviceId: upsertDeviceId } },
|
||||||
create: {
|
create: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
deviceId,
|
deviceId: upsertDeviceId,
|
||||||
platform: devicePlatform,
|
platform: devicePlatform,
|
||||||
model: model ?? null,
|
model: model ?? null,
|
||||||
name: hostname,
|
name: hostname,
|
||||||
osVersion: osVersion ?? null,
|
osVersion: osVersion ?? null,
|
||||||
|
hardwareId: hardwareId ?? null,
|
||||||
magicDnsToken: dnsToken,
|
magicDnsToken: dnsToken,
|
||||||
magicEnrolledAt: new Date(),
|
magicEnrolledAt: new Date(),
|
||||||
magicHostname: hostname,
|
magicHostname: hostname,
|
||||||
magicRemovalPassword: removalPassword,
|
magicRemovalPassword: removalPassword,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
|
hardwareId: hardwareId ?? undefined,
|
||||||
magicDnsToken: dnsToken,
|
magicDnsToken: dnsToken,
|
||||||
magicEnrolledAt: new Date(),
|
magicEnrolledAt: new Date(),
|
||||||
magicRevokedAt: null, // Clear falls vorher revoked
|
magicRevokedAt: null, // Clear falls vorher revoked
|
||||||
|
|||||||
@ -418,6 +418,7 @@ export async function deleteUserDevice(
|
|||||||
|
|
||||||
export interface MagicDeviceRecord {
|
export interface MagicDeviceRecord {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
|
hardwareId: string | null;
|
||||||
hostname: string | null;
|
hostname: string | null;
|
||||||
model: string | null;
|
model: string | null;
|
||||||
osVersion: string | null;
|
osVersion: string | null;
|
||||||
@ -445,6 +446,7 @@ export async function listMagicDevices(
|
|||||||
orderBy: { magicEnrolledAt: "desc" },
|
orderBy: { magicEnrolledAt: "desc" },
|
||||||
select: {
|
select: {
|
||||||
deviceId: true,
|
deviceId: true,
|
||||||
|
hardwareId: true,
|
||||||
magicHostname: true,
|
magicHostname: true,
|
||||||
model: true,
|
model: true,
|
||||||
osVersion: true,
|
osVersion: true,
|
||||||
@ -458,6 +460,7 @@ export async function listMagicDevices(
|
|||||||
|
|
||||||
return devices.map((d) => ({
|
return devices.map((d) => ({
|
||||||
deviceId: d.deviceId,
|
deviceId: d.deviceId,
|
||||||
|
hardwareId: d.hardwareId,
|
||||||
hostname: d.magicHostname,
|
hostname: d.magicHostname,
|
||||||
model: d.model,
|
model: d.model,
|
||||||
osVersion: d.osVersion,
|
osVersion: d.osVersion,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user