443 lines
17 KiB
Swift
443 lines
17 KiB
Swift
import SwiftUI
|
||
import AuthenticationServices
|
||
import Alamofire
|
||
import CryptoKit
|
||
|
||
/// 主登录视图 - 处理苹果登录
|
||
struct LoginView: View {
|
||
// MARK: - Properties
|
||
|
||
@State private var isLoading = false
|
||
@State private var showError = false
|
||
@State private var errorMessage = ""
|
||
@State private var currentNonce: String?
|
||
@State private var isLoggedIn = false
|
||
// MARK: - Body
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
ZStack {
|
||
// Background
|
||
Color(red: 1.0, green: 0.67, blue: 0.15)
|
||
.edgesIgnoringSafeArea(.all)
|
||
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text("Hi, I'm MeMo!")
|
||
.font(.largeTitle)
|
||
.fontWeight(.semibold)
|
||
.foregroundColor(.black)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.padding(.leading, 24)
|
||
.padding(.top, 44)
|
||
|
||
Text("Welcome~")
|
||
.font(.largeTitle)
|
||
.fontWeight(.semibold)
|
||
.foregroundColor(.black)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.padding(.leading, 24)
|
||
.padding(.bottom, 20)
|
||
|
||
Spacer()
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||
|
||
VStack(spacing: 16) {
|
||
Spacer()
|
||
signInButton()
|
||
termsAndPrivacyView()
|
||
}
|
||
.padding()
|
||
.alert(isPresented: $showError) {
|
||
Alert(
|
||
title: Text("Error"),
|
||
message: Text(errorMessage),
|
||
dismissButton: .default(Text("OK"))
|
||
)
|
||
}
|
||
|
||
if isLoading {
|
||
loadingView()
|
||
}
|
||
}
|
||
.navigationBarHidden(true)
|
||
.fullScreenCover(isPresented: $isLoggedIn) {
|
||
NavigationStack {
|
||
UserInfo()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Views
|
||
|
||
private func signInButton() -> some View {
|
||
SignInWithAppleButton(
|
||
onRequest: { request in
|
||
let nonce = String.randomURLSafeString(length: 32)
|
||
self.currentNonce = nonce
|
||
request.requestedScopes = [.fullName, .email]
|
||
request.nonce = self.sha256(nonce)
|
||
},
|
||
onCompletion: handleAppleSignIn
|
||
)
|
||
.signInWithAppleButtonStyle(.white)
|
||
.frame(height: 50)
|
||
.cornerRadius(25)
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 25)
|
||
.stroke(Color.black, lineWidth: 1)
|
||
)
|
||
}
|
||
|
||
private func termsAndPrivacyView() -> some View {
|
||
VStack(spacing: 4) {
|
||
HStack {
|
||
Text("By continuing, you agree to our")
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
|
||
Button("Terms of") {
|
||
openURL("https://yourwebsite.com/terms")
|
||
}
|
||
.font(.caption2)
|
||
.foregroundColor(.blue)
|
||
}
|
||
.multilineTextAlignment(.center)
|
||
.padding(.horizontal, 24)
|
||
|
||
HStack(spacing: 8) {
|
||
Button("Service") {
|
||
openURL("https://yourwebsite.com/terms")
|
||
}
|
||
.font(.caption2)
|
||
.foregroundColor(.blue)
|
||
|
||
Text("and")
|
||
.foregroundColor(.secondary)
|
||
.font(.caption)
|
||
|
||
Button("Privacy Policy") {
|
||
openURL("https://yourwebsite.com/privacy")
|
||
}
|
||
.font(.caption2)
|
||
.foregroundColor(.blue)
|
||
}
|
||
.padding(.top, 4)
|
||
}
|
||
.fixedSize(horizontal: false, vertical: true)
|
||
.frame(maxWidth: .infinity)
|
||
.padding(.horizontal, 24)
|
||
.padding(.bottom, 24)
|
||
}
|
||
|
||
private func loadingView() -> some View {
|
||
ZStack {
|
||
Color.black.opacity(0.4)
|
||
.edgesIgnoringSafeArea(.all)
|
||
|
||
ProgressView()
|
||
.scaleEffect(1.5)
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
}
|
||
}
|
||
|
||
// MARK: - Authentication
|
||
|
||
private func handleAppleSignIn(result: Result<ASAuthorization, Error>) {
|
||
print("🔵 [Apple Sign In] 开始处理登录结果...")
|
||
DispatchQueue.main.async {
|
||
self.isLoggedIn = true
|
||
}
|
||
switch result {
|
||
case .success(let authResults):
|
||
print("✅ [Apple Sign In] 登录授权成功")
|
||
processAppleIDCredential(authResults.credential)
|
||
case .failure(let error):
|
||
print("❌ [Apple Sign In] 登录失败: \(error.localizedDescription)")
|
||
handleSignInError(error)
|
||
}
|
||
}
|
||
|
||
private func processAppleIDCredential(_ credential: ASAuthorizationCredential) {
|
||
print("🔵 [Apple ID] 开始处理凭证...")
|
||
guard let appleIDCredential = credential as? ASAuthorizationAppleIDCredential else {
|
||
print("❌ [Apple ID] 凭证类型不匹配")
|
||
showError(message: "无法处理Apple ID凭证")
|
||
return
|
||
}
|
||
|
||
let userId = appleIDCredential.user
|
||
let email = appleIDCredential.email ?? ""
|
||
let fullName = [
|
||
appleIDCredential.fullName?.givenName,
|
||
appleIDCredential.fullName?.familyName
|
||
]
|
||
.compactMap { $0 }
|
||
.joined(separator: " ")
|
||
|
||
print("ℹ️ [Apple ID] 用户数据 - ID: \(userId), 邮箱: \(email.isEmpty ? "未提供" : email), 姓名: \(fullName.isEmpty ? "未提供" : fullName)")
|
||
|
||
guard let identityTokenData = appleIDCredential.identityToken,
|
||
let identityToken = String(data: identityTokenData, encoding: .utf8) else {
|
||
print("❌ [Apple ID] 无法获取身份令牌")
|
||
showError(message: "无法获取身份令牌")
|
||
return
|
||
}
|
||
|
||
var authCode: String? = nil
|
||
if let authCodeData = appleIDCredential.authorizationCode {
|
||
authCode = String(data: authCodeData, encoding: .utf8)
|
||
print("ℹ️ [Apple ID] 获取到授权码")
|
||
} else {
|
||
print("ℹ️ [Apple ID] 未获取到授权码(可选)")
|
||
}
|
||
|
||
print("🔵 [Apple ID] 准备调用后端认证...")
|
||
authenticateWithBackend(
|
||
userId: appleIDCredential.user,
|
||
email: appleIDCredential.email ?? "",
|
||
name: [appleIDCredential.fullName?.givenName,
|
||
appleIDCredential.fullName?.familyName]
|
||
.compactMap { $0 }
|
||
.joined(separator: " "),
|
||
identityToken: identityToken,
|
||
authCode: authCode
|
||
)
|
||
}
|
||
|
||
// MARK: - Network
|
||
|
||
private func authenticateWithBackend(
|
||
userId: String,
|
||
email: String,
|
||
name: String,
|
||
identityToken: String,
|
||
authCode: String?
|
||
) {
|
||
isLoading = true
|
||
print("🔵 [Backend] 开始后端认证...")
|
||
|
||
let endpoint = "\(APIConfig.baseURL)/iam/login/oauth"
|
||
guard let url = URL(string: endpoint) else {
|
||
print("❌ [Backend] 无效的URL: \(endpoint)")
|
||
self.handleAuthenticationError(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的URL"]))
|
||
return
|
||
}
|
||
|
||
var parameters: [String: Any] = [
|
||
"provider": "Apple",
|
||
"token": identityToken,
|
||
"userId": userId,
|
||
"email": email,
|
||
"name": name,
|
||
]
|
||
|
||
if let authCode = authCode {
|
||
parameters["authorization_code"] = authCode
|
||
}
|
||
|
||
print("📤 [Backend] 请求URL: \(endpoint)")
|
||
print("📤 [Backend] 请求参数: \(parameters)")
|
||
|
||
var request = URLRequest(url: url)
|
||
request.httpMethod = "POST"
|
||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||
|
||
do {
|
||
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
|
||
} catch {
|
||
print("❌ [Backend] 参数序列化失败: \(error.localizedDescription)")
|
||
self.handleAuthenticationError(error)
|
||
return
|
||
}
|
||
|
||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||
DispatchQueue.main.async {
|
||
self.isLoading = false
|
||
|
||
// 1. 处理网络错误
|
||
if let error = error {
|
||
print("❌ [Backend] 请求失败: \(error.localizedDescription)")
|
||
self.handleAuthenticationError(error)
|
||
return
|
||
}
|
||
|
||
// 2. 检查响应
|
||
guard let httpResponse = response as? HTTPURLResponse else {
|
||
let error = NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的服务器响应"])
|
||
print("❌ [Backend] \(error.localizedDescription)")
|
||
self.handleAuthenticationError(error)
|
||
return
|
||
}
|
||
|
||
// 3. 打印响应状态码和头部信息
|
||
let statusCode = httpResponse.statusCode
|
||
print("""
|
||
📥 [Backend] 响应信息:
|
||
- 状态码: \(statusCode)
|
||
- URL: \(httpResponse.url?.absoluteString ?? "N/A")
|
||
- Headers: \(httpResponse.allHeaderFields)
|
||
""")
|
||
|
||
// 4. 处理响应数据
|
||
if let data = data {
|
||
if let jsonString = String(data: data, encoding: .utf8) {
|
||
print("📦 [Backend] 响应内容: \(jsonString)")
|
||
}
|
||
|
||
// 5. 解析响应数据
|
||
do {
|
||
// 首先解析顶层 JSON
|
||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||
print("✅ [Backend] 响应解析成功")
|
||
print("📦 [Backend] 响应内容: \(json)")
|
||
|
||
// 检查状态码
|
||
if let code = json["code"] as? Int, code != 0 {
|
||
let errorMsg = json["message"] as? String ?? "未知错误"
|
||
print("❌ [Backend] 请求失败: \(errorMsg)")
|
||
self.showError(message: errorMsg)
|
||
return
|
||
}
|
||
|
||
// 获取 data 字典
|
||
guard let responseData = json["data"] as? [String: Any] else {
|
||
print("⚠️ [Backend] 未找到 data 字段")
|
||
self.showError(message: "服务器返回数据格式错误")
|
||
return
|
||
}
|
||
|
||
// 获取 user_login_info 字典
|
||
if let userLoginInfo = responseData["user_login_info"] as? [String: Any] {
|
||
print("👤 [Backend] 用户登录信息: \(userLoginInfo)")
|
||
|
||
// 保存令牌到 Keychain
|
||
if let accessToken = userLoginInfo["access_token"] as? String {
|
||
_ = KeychainHelper.saveAccessToken(accessToken)
|
||
print("🔑 [Keychain] 访问令牌已保存")
|
||
|
||
// 保存用户信息到 UserDefaults
|
||
var userInfo: [String: Any] = [
|
||
"user_id": userLoginInfo["user_id"] as? Int64 ?? 0,
|
||
"account": userLoginInfo["account"] as? String ?? "",
|
||
"nickname": userLoginInfo["nickname"] as? String ?? "",
|
||
"avatar": userLoginInfo["avatar_file_url"] as? String ?? ""
|
||
]
|
||
UserDefaults.standard.set(userInfo, forKey: "currentUserInfo")
|
||
print("👤 [UserDefaults] 用户信息已保存")
|
||
}
|
||
|
||
if let refreshToken = userLoginInfo["refresh_token"] as? String {
|
||
_ = KeychainHelper.saveRefreshToken(refreshToken)
|
||
print("🔄 [Keychain] 刷新令牌已保存")
|
||
}
|
||
|
||
// 处理认证成功
|
||
DispatchQueue.main.async {
|
||
self.handleSuccessfulAuthentication()
|
||
}
|
||
return
|
||
} else {
|
||
print("⚠️ [Backend] 未找到 user_login_info 字段")
|
||
self.showError(message: "登录信息不完整")
|
||
}
|
||
}
|
||
} catch {
|
||
print("⚠️ [Backend] 响应解析失败: \(error.localizedDescription)")
|
||
self.showError(message: "数据解析失败")
|
||
}
|
||
}
|
||
|
||
// 6. 如果上面的 return 没有执行,说明认证失败
|
||
if statusCode < 200 || statusCode >= 300 {
|
||
let errorMessage: String
|
||
switch statusCode {
|
||
case 400:
|
||
errorMessage = "请求参数错误"
|
||
case 401:
|
||
errorMessage = "认证失败,请重新登录"
|
||
case 403:
|
||
errorMessage = "权限不足"
|
||
case 404:
|
||
errorMessage = "请求的接口不存在"
|
||
case 500...599:
|
||
errorMessage = "服务器内部错误,请稍后重试"
|
||
default:
|
||
errorMessage = "未知错误 (状态码: \(statusCode))"
|
||
}
|
||
self.showError(message: errorMessage)
|
||
}
|
||
}
|
||
}
|
||
|
||
task.resume()
|
||
}
|
||
|
||
// MARK: - Helpers
|
||
|
||
private func handleSuccessfulAuthentication() {
|
||
print("✅ [Auth] 登录成功,准备跳转到用户信息页面...")
|
||
DispatchQueue.main.async {
|
||
self.isLoggedIn = true
|
||
}
|
||
}
|
||
|
||
private func handleSignInError(_ error: Error) {
|
||
let errorMessage = (error as NSError).localizedDescription
|
||
print("❌ [Auth] 登录错误: \(errorMessage)")
|
||
showError(message: "登录失败: \(error.localizedDescription)")
|
||
}
|
||
|
||
private func handleAuthenticationError(_ error: Error) {
|
||
let errorMessage = error.localizedDescription
|
||
print("❌ [Auth] 认证错误: \(errorMessage)")
|
||
DispatchQueue.main.async {
|
||
self.isLoggedIn = false
|
||
self.showError(message: "登录失败: \(errorMessage)")
|
||
}
|
||
}
|
||
|
||
private func showError(message: String) {
|
||
DispatchQueue.main.async {
|
||
self.errorMessage = message
|
||
self.showError = true
|
||
}
|
||
}
|
||
|
||
private func openURL(_ string: String) {
|
||
guard let url = URL(string: string) else { return }
|
||
UIApplication.shared.open(url)
|
||
}
|
||
|
||
private func sha256(_ input: String) -> String {
|
||
let inputData = Data(input.utf8)
|
||
let hashedData = SHA256.hash(data: inputData)
|
||
return hashedData.compactMap { String(format: "%02x", $0) }.joined()
|
||
}
|
||
}
|
||
|
||
// MARK: - Extensions
|
||
|
||
extension String {
|
||
static func randomURLSafeString(length: Int) -> String {
|
||
let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"
|
||
var randomString = ""
|
||
|
||
for _ in 0..<length {
|
||
let randomIndex = Int.random(in: 0..<characters.count)
|
||
let character = characters[characters.index(characters.startIndex, offsetBy: randomIndex)]
|
||
randomString.append(character)
|
||
}
|
||
|
||
return randomString
|
||
}
|
||
}
|
||
|
||
// MARK: - Preview
|
||
|
||
struct LoginView_Previews: PreviewProvider {
|
||
static var previews: some View {
|
||
LoginView()
|
||
}
|
||
} |