diff --git a/PulseHealth/Services/APIService.swift b/PulseHealth/Services/APIService.swift index 3acdfe7..9a78bb9 100644 --- a/PulseHealth/Services/APIService.swift +++ b/PulseHealth/Services/APIService.swift @@ -1,12 +1,17 @@ import Foundation enum APIError: Error, LocalizedError { - case unauthorized, networkError, decodingError + case unauthorized + case networkError(String) + case decodingError(String) + case serverError(Int, String) + var errorDescription: String? { switch self { case .unauthorized: return "Неверный email или пароль" - case .networkError: return "Ошибка сети" - case .decodingError: return "Ошибка данных" + case .networkError(let msg): return "Ошибка сети: \(msg)" + case .decodingError(let msg): return "Ошибка данных: \(msg)" + case .serverError(let code, let msg): return "Ошибка сервера \(code): \(msg)" } } } @@ -15,38 +20,68 @@ class APIService { static let shared = APIService() let baseURL = "https://health.digital-home.site" + private func makeRequest(url: URL, method: String = "GET", token: String? = nil, body: Data? = nil) -> URLRequest { + var req = URLRequest(url: url) + req.httpMethod = method + req.setValue("application/json", forHTTPHeaderField: "Content-Type") + req.timeoutInterval = 15 + if let token = token { + req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + req.httpBody = body + return req + } + func login(email: String, password: String) async throws -> LoginResponse { let url = URL(string: "\(baseURL)/api/auth/login")! - var req = URLRequest(url: url) - req.httpMethod = "POST" - req.setValue("application/json", forHTTPHeaderField: "Content-Type") - req.httpBody = try JSONEncoder().encode(LoginRequest(email: email, password: password)) + let body = try JSONEncoder().encode(LoginRequest(email: email, password: password)) + let req = makeRequest(url: url, method: "POST", body: body) + let (data, response) = try await URLSession.shared.data(for: req) - guard let r = response as? HTTPURLResponse, r.statusCode == 200 else { throw APIError.unauthorized } - return try JSONDecoder().decode(LoginResponse.self, from: data) + guard let httpResponse = response as? HTTPURLResponse else { + throw APIError.networkError("Нет ответа от сервера") + } + + if httpResponse.statusCode == 401 { + throw APIError.unauthorized + } + + if httpResponse.statusCode != 200 { + let msg = String(data: data, encoding: .utf8) ?? "Unknown" + throw APIError.serverError(httpResponse.statusCode, msg) + } + + do { + return try JSONDecoder().decode(LoginResponse.self, from: data) + } catch { + throw APIError.decodingError(error.localizedDescription) + } } func getProfile(token: String) async throws -> ProfileResponse { let url = URL(string: "\(baseURL)/api/profile")! - var req = URLRequest(url: url) - req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + let req = makeRequest(url: url, token: token) let (data, _) = try await URLSession.shared.data(for: req) return try JSONDecoder().decode(ProfileResponse.self, from: data) } func getReadiness(token: String) async throws -> ReadinessResponse { let url = URL(string: "\(baseURL)/api/health/readiness")! - var req = URLRequest(url: url) - req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - let (data, _) = try await URLSession.shared.data(for: req) + let req = makeRequest(url: url, token: token) + let (data, response) = try await URLSession.shared.data(for: req) + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + throw APIError.networkError("Readiness недоступен") + } return try JSONDecoder().decode(ReadinessResponse.self, from: data) } func getLatest(token: String) async throws -> LatestHealthResponse { let url = URL(string: "\(baseURL)/api/health/latest")! - var req = URLRequest(url: url) - req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - let (data, _) = try await URLSession.shared.data(for: req) + let req = makeRequest(url: url, token: token) + let (data, response) = try await URLSession.shared.data(for: req) + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + throw APIError.networkError("Latest недоступен") + } return try JSONDecoder().decode(LatestHealthResponse.self, from: data) } } diff --git a/PulseHealth/Views/LoginView.swift b/PulseHealth/Views/LoginView.swift index 34d6574..496dcf7 100644 --- a/PulseHealth/Views/LoginView.swift +++ b/PulseHealth/Views/LoginView.swift @@ -2,53 +2,110 @@ import SwiftUI struct LoginView: View { @EnvironmentObject var authManager: AuthManager - @State private var email = "" + @State private var email = "daniilklimov25@gmail.com" @State private var password = "" @State private var isLoading = false @State private var errorMessage = "" + @State private var showPassword = false var body: some View { ZStack { - LinearGradient(colors: [Color(hex: "1a1a2e"), Color(hex: "16213e")], startPoint: .top, endPoint: .bottom) + LinearGradient(colors: [Color(hex: "1a1a2e"), Color(hex: "16213e")], + startPoint: .top, endPoint: .bottom) .ignoresSafeArea() + VStack(spacing: 32) { VStack(spacing: 8) { Text("🫀").font(.system(size: 60)) Text("Pulse Health").font(.largeTitle.bold()).foregroundColor(.white) - Text("Персональный дашборд здоровья").font(.subheadline).foregroundColor(.white.opacity(0.6)) + Text("Персональный дашборд здоровья") + .font(.subheadline).foregroundColor(.white.opacity(0.6)) }.padding(.top, 60) + VStack(spacing: 16) { TextField("Email", text: $email) - .keyboardType(.emailAddress).autocapitalization(.none) - .padding().background(Color.white.opacity(0.1)).cornerRadius(12).foregroundColor(.white) - SecureField("Пароль", text: $password) - .padding().background(Color.white.opacity(0.1)).cornerRadius(12).foregroundColor(.white) + .keyboardType(.emailAddress) + .autocapitalization(.none) + .autocorrectionDisabled() + .padding() + .background(Color.white.opacity(0.1)) + .cornerRadius(12) + .foregroundColor(.white) + + // Password field with show/hide toggle + HStack { + Group { + if showPassword { + TextField("Пароль", text: $password) + .autocapitalization(.none) + .autocorrectionDisabled() + } else { + SecureField("Пароль", text: $password) + } + } + .foregroundColor(.white) + + Button(action: { showPassword.toggle() }) { + Image(systemName: showPassword ? "eye.slash.fill" : "eye.fill") + .foregroundColor(.white.opacity(0.6)) + } + } + .padding() + .background(Color.white.opacity(0.1)) + .cornerRadius(12) + if !errorMessage.isEmpty { - Text(errorMessage).foregroundColor(.red).font(.caption) + Text(errorMessage) + .foregroundColor(.red) + .font(.caption) + .multilineTextAlignment(.center) + .padding(.horizontal) } + Button(action: login) { - if isLoading { ProgressView().tint(.white) } - else { Text("Войти").font(.headline).foregroundColor(.black) } + if isLoading { + ProgressView().tint(.black) + } else { + Text("Войти").font(.headline).foregroundColor(.black) + } } - .frame(maxWidth: .infinity).padding() - .background(Color(hex: "00d4aa")).cornerRadius(12).disabled(isLoading) - }.padding(.horizontal, 24) + .frame(maxWidth: .infinity) + .padding() + .background(Color(hex: "00d4aa")) + .cornerRadius(12) + .disabled(isLoading || email.isEmpty || password.isEmpty) + } + .padding(.horizontal, 24) + Spacer() } } } func login() { - isLoading = true; errorMessage = "" + isLoading = true + errorMessage = "" Task { do { - let response = try await APIService.shared.login(email: email, password: password) - let profile = try await APIService.shared.getProfile(token: response.token) + let response = try await APIService.shared.login(email: email.trimmingCharacters(in: .whitespaces), password: password) + let profile = try? await APIService.shared.getProfile(token: response.token) await MainActor.run { - authManager.login(token: response.token, name: response.user.name, apiKey: profile.apiKey ?? "") + authManager.login( + token: response.token, + name: response.user.name, + apiKey: profile?.apiKey ?? "" + ) + } + } catch let error as APIError { + await MainActor.run { + errorMessage = error.errorDescription ?? "Ошибка" + isLoading = false } } catch { - await MainActor.run { errorMessage = error.localizedDescription; isLoading = false } + await MainActor.run { + errorMessage = "Ошибка: \(error.localizedDescription)" + isLoading = false + } } } }