chahinebrini 8670b45351 fix(magic): inline mobileconfig template as TS constant
serverAssets approach didn't bundle the template into the Nitro
output (no .output-staging/server/chunks/raw/ dir, no asset-storage
mount in nitro.mjs). Logs confirm: '[Magic] Profile template missing
in serverAssets'.

Drop serverAssets entirely. Inline the template (~2KB) as a TS
constant in backend/server/utils/magic-profile-template.ts. Build-
robust, no FS/storage dependency at runtime. Canonical source of
truth remains ops/mdm/rebreak-mac-dns-filter.mobileconfig — keep in
sync manually until/unless we add a codegen step.
2026-06-03 09:57:27 +02:00

228 lines
7.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import AppKit
import SwiftUI
struct LoginView: View {
@State private var digits: [String] = Array(repeating: "", count: 6)
@FocusState private var focusedField: Int?
@State private var isLoading = false
@State private var errorMessage: String?
let onSuccess: (AuthSession) -> Void
private var enteredCode: String { digits.joined() }
private var isComplete: Bool { enteredCode.count == 6 && enteredCode.allSatisfy(\.isNumber) }
var body: some View {
VStack(spacing: 28) {
Spacer().frame(height: 20)
// App-Icon + Header
VStack(spacing: 14) {
appIconView
.frame(width: 84, height: 84)
Text("Rebreak Magic")
.font(.title.bold())
VStack(spacing: 4) {
Text("Mit der Rebreak-App verbinden")
.font(.subheadline)
.foregroundStyle(.secondary)
Text("Öffne in der App: Einstellungen → Rebreak Magic")
.font(.caption)
.foregroundStyle(.tertiary)
}
}
// 6-stelliger Code-Input
VStack(spacing: 14) {
HStack(spacing: 10) {
ForEach(0..<6, id: \.self) { index in
digitField(index: index)
}
}
if let error = errorMessage {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
Text(error)
.font(.caption)
.foregroundStyle(.red)
}
}
Button(action: handleSubmit) {
HStack(spacing: 8) {
if isLoading {
ProgressView()
.controlSize(.small)
.tint(.white)
}
Text(isLoading ? "Verbinde…" : "Verbinden")
.fontWeight(.medium)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 6)
}
.buttonStyle(.borderedProminent)
.keyboardShortcut(.return, modifiers: [])
.disabled(!isComplete || isLoading)
}
.frame(maxWidth: 380)
Spacer()
Text("Noch keine Rebreak-App? Lade sie im App Store / Play Store.")
.font(.caption2)
.foregroundStyle(.tertiary)
.padding(.bottom, 18)
}
.padding(.horizontal, 32)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(nsColor: .windowBackgroundColor))
.onAppear { focusedField = 0 }
}
// MARK: - Components
@ViewBuilder
private var appIconView: some View {
if let icon = NSApplication.shared.applicationIconImage,
icon.size.width > 2 {
Image(nsImage: icon)
.resizable()
.interpolation(.high)
.frame(width: 84, height: 84)
} else {
// Fallback: gefärbtes RoundedRect (entspricht macOS-Stil)
RoundedRectangle(cornerRadius: 18, style: .continuous)
.fill(LinearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing))
.overlay(
Image(systemName: "wand.and.stars")
.font(.system(size: 40, weight: .semibold))
.foregroundStyle(.white)
)
}
}
@ViewBuilder
private func digitField(index: Int) -> some View {
TextField("", text: Binding(
get: { digits[index] },
set: { newValue in handleDigitInput(newValue, at: index) }
))
.textFieldStyle(.plain)
.multilineTextAlignment(.center)
.font(.system(size: 28, weight: .semibold, design: .rounded))
.frame(width: 48, height: 60)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color(nsColor: .controlBackgroundColor))
)
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.strokeBorder(
focusedField == index ? Color.accentColor : Color.gray.opacity(0.3),
lineWidth: focusedField == index ? 2 : 1
)
)
.focused($focusedField, equals: index)
}
// MARK: - Logic
private func handleDigitInput(_ raw: String, at index: Int) {
// Erlaubt: 09. Mehrere Zeichen kann Paste sein ODER User tippt in
// ein bereits gefülltes Feld (newValue = "alt+neu").
let onlyDigits = raw.filter(\.isNumber)
let previous = digits[index]
// Paste-Heuristik: 2+ Ziffern UND keine davon ist die alte Ziffer am Anfang,
// oder Länge > 2. Sonst: User hat in ein gefülltes Feld eine neue Ziffer
// getippt letzte Ziffer als neuen Wert nehmen.
let isPaste: Bool = {
if onlyDigits.count >= 3 { return true }
if onlyDigits.count == 2 {
// Wenn beide Ziffern unterschiedlich sind und das erste Zeichen
// dem bisherigen Wert entspricht Replace-Tipp, kein Paste.
if !previous.isEmpty && onlyDigits.first.map(String.init) == previous {
return false
}
return true
}
return false
}()
if isPaste {
// Paste / Auto-Fill: über Felder ab `index` verteilen
let chars = Array(onlyDigits.prefix(6 - index))
for (offset, ch) in chars.enumerated() {
let target = index + offset
if target < 6 {
digits[target] = String(ch)
}
}
let nextFocus = min(index + chars.count, 5)
advanceFocus(to: nextFocus)
if digits.allSatisfy({ !$0.isEmpty }) && !isLoading {
handleSubmit()
}
return
}
if onlyDigits.isEmpty {
// Backspace
digits[index] = ""
if index > 0 {
advanceFocus(to: index - 1)
}
return
}
// Single-digit Eingabe (oder Replace in gefülltes Feld letzte Ziffer nehmen)
let newDigit = String(onlyDigits.suffix(1))
digits[index] = newDigit
if index < 5 {
advanceFocus(to: index + 1)
} else if isComplete && !isLoading {
// Letztes Feld gefüllt automatisch absenden
handleSubmit()
}
}
/// Focus-Wechsel muss async passieren, sonst kollidiert er mit dem laufenden
/// TextField-Edit-Cycle und der Focus springt nicht zuverlässig.
private func advanceFocus(to target: Int) {
DispatchQueue.main.async {
focusedField = target
}
}
private func handleSubmit() {
guard isComplete, !isLoading else { return }
let code = enteredCode
Task {
isLoading = true
errorMessage = nil
do {
let session = try await AuthService.shared.signInWithPairingCode(code)
onSuccess(session)
} catch {
errorMessage = error.localizedDescription
// Felder leeren bei Fehler
digits = Array(repeating: "", count: 6)
focusedField = 0
}
isLoading = false
}
}
}
#Preview {
LoginView { session in
print("Magic-Session: \(session.sessionId)")
}
.frame(width: 720, height: 600)
}