Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit 3e89f3a

Browse files
authored
Use GitHub PAT to access certificates
1 parent 8f6c617 commit 3e89f3a

1 file changed

Lines changed: 91 additions & 32 deletions

File tree

Sources/prosign/views/CertificateView.swift

Lines changed: 91 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import SwiftUI
22
import UniformTypeIdentifiers
33
import ProStoreTools
4+
import ZIPFoundation
45
// Centralized types to avoid conflicts
56
struct CertificateFileItem {
67
var name: String = ""
@@ -12,16 +13,29 @@ struct CustomCertificate: Identifiable {
1213
let folderName: String
1314
}
1415
// MARK: - Release Models
15-
struct Release: Codable, Identifiable, Equatable {
16+
struct Release: Codable, Identifiable, Equatable, Hashable {
1617
let id: Int
1718
let name: String
1819
let tagName: String
19-
let publishedAt: Date
20+
let publishedAt: String
2021
let assets: [Asset]
22+
23+
enum CodingKeys: String, CodingKey {
24+
case id, name, tagName = "tag_name", publishedAt = "published_at", assets
25+
}
26+
27+
var publishedDate: Date {
28+
// Will be handled by decoder
29+
Date()
30+
}
2131
}
2232
struct Asset: Codable {
2333
let name: String
2434
let browserDownloadUrl: String
35+
36+
enum CodingKeys: String, CodingKey {
37+
case name, browserDownloadUrl = "browser_download_url"
38+
}
2539
}
2640
// MARK: - Date Extension for Formatting
2741
extension Date {
@@ -81,9 +95,9 @@ struct OfficialCertificatesView: View {
8195
Form {
8296
Section("Select Official Certificate") {
8397
Picker("Certificate", selection: $selectedRelease) {
84-
Text("Select a certificate").tag(Release?.none)
98+
Text("Select a certificate").tag(nil as Release?)
8599
ForEach(releases) { release in
86-
Text(cleanName(release.name)).tag(Optional(release))
100+
Text(cleanName(release.name)).tag(release as Release?)
87101
}
88102
}
89103
}
@@ -95,7 +109,7 @@ struct OfficialCertificatesView: View {
95109
if let release = selectedRelease {
96110
Section("Details") {
97111
Text("Tag: \(release.tagName)")
98-
Text("Published: \(release.publishedAt, formatter: dateFormatter)")
112+
Text("Published: \(dateFormatter.string(from: isoDate(string: release.publishedAt)))")
99113
}
100114
}
101115
Section {
@@ -112,7 +126,7 @@ struct OfficialCertificatesView: View {
112126
Button("Add Certificate") {
113127
addCertificate()
114128
}
115-
.disabled(p12Data == nil || isChecking)
129+
.disabled(p12Data == nil || provData == nil || password == nil || isChecking)
116130
}
117131
}
118132
.navigationTitle("Official Certificates")
@@ -130,20 +144,47 @@ struct OfficialCertificatesView: View {
130144
}
131145
}
132146

147+
private func isoDate(string: String) -> Date {
148+
let formatter = ISO8601DateFormatter()
149+
return formatter.date(from: string) ?? Date()
150+
}
151+
133152
private func cleanName(_ name: String) -> String {
134153
name.replacingOccurrences(of: "\\\\", with: "").replacingOccurrences(of: "\\", with: "")
135154
}
136155

156+
private func getPAT() async -> String? {
157+
guard let url = URL(string: "https://certapi.loyah.dev/pac") else { return nil }
158+
do {
159+
let (data, _) = try await URLSession.shared.data(from: url)
160+
return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
161+
} catch {
162+
return nil
163+
}
164+
}
165+
137166
private func fetchReleases() {
138-
guard let url = URL(string: "https://api.github.com/repos/loyahdev/certificates/releases") else { return }
139167
Task {
168+
let pat = await getPAT()
169+
let url = URL(string: "https://api.github.com/repos/loyahdev/certificates/releases")!
170+
var request = URLRequest(url: url)
171+
if let pat = pat {
172+
request.setValue("token \(pat)", forHTTPHeaderField: "Authorization")
173+
}
140174
do {
141-
let (data, _) = try await URLSession.shared.data(from: url)
175+
let (data, response) = try await URLSession.shared.data(for: request)
176+
var decodeData = data
177+
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200, pat != nil {
178+
// Fallback to unauthenticated
179+
var fallbackRequest = URLRequest(url: url)
180+
let (fallbackData, _) = try await URLSession.shared.data(for: fallbackRequest)
181+
decodeData = fallbackData
182+
}
142183
let decoder = JSONDecoder()
143-
decoder.dateDecodingStrategy = .iso8601
144-
let decoded = try decoder.decode([Release].self, from: data)
184+
decoder.dateDecodingStrategy = .deferredToDate
185+
let decoded = try decoder.decode([Release].self, from: decodeData)
145186
await MainActor.run {
146-
self.releases = decoded.sorted { $0.publishedAt > $1.publishedAt }
187+
self.releases = decoded.sorted { isoDate(string: $0.publishedAt) > isoDate(string: $1.publishedAt) }
147188
}
148189
} catch {
149190
await MainActor.run {
@@ -156,47 +197,64 @@ struct OfficialCertificatesView: View {
156197
private func checkCertificate() {
157198
guard let release = selectedRelease,
158199
let asset = release.assets.first(where: { $0.name.hasSuffix(".zip") }),
159-
let downloadUrl = URL(string: asset.browserDownloadUrl) else {
200+
let downloadStr = asset.browserDownloadUrl,
201+
let downloadUrl = URL(string: downloadStr) else {
160202
statusMessage = "Invalid release"
161203
return
162204
}
163205
isChecking = true
164206
statusMessage = "Downloading..."
165207
Task {
208+
let pat = await getPAT()
209+
var downloadRequest = URLRequest(url: downloadUrl)
210+
if let pat = pat {
211+
downloadRequest.setValue("token \(pat)", forHTTPHeaderField: "Authorization")
212+
}
166213
do {
167-
let (tempData, _) = try await URLSession.shared.data(from: downloadUrl)
214+
var tempData = Data()
215+
var response = URLResponse()
216+
(tempData, response) = try await URLSession.shared.data(for: downloadRequest)
217+
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200, pat != nil {
218+
// Fallback
219+
var fallbackRequest = URLRequest(url: downloadUrl)
220+
(tempData, _) = try await URLSession.shared.data(for: fallbackRequest)
221+
}
168222
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
169223
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true, attributes: nil)
224+
defer {
225+
try? FileManager.default.removeItem(at: tempDir)
226+
}
170227
let zipPath = tempDir.appendingPathComponent("temp.zip")
171228
try tempData.write(to: zipPath)
172229
let extractDir = tempDir.appendingPathComponent("extracted")
173230
try FileManager.default.unzipItem(at: zipPath, to: extractDir, progress: nil)
174231
// Find files
175232
var p12Urls: [URL] = []
176233
var provUrls: [URL] = []
177-
if let enumerator = FileManager.default.enumerator(at: extractDir, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) {
234+
if let enumerator = FileManager.default.enumerator(at: extractDir, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]) {
178235
for case let fileURL as URL in enumerator {
179236
let path = fileURL.path
180-
if path.hasSuffix(".p12") {
181-
p12Urls.append(fileURL)
182-
} else if path.hasSuffix(".mobileprovision") {
183-
provUrls.append(fileURL)
237+
if !path.contains("__MACOSX") {
238+
if path.hasSuffix(".p12") {
239+
p12Urls.append(fileURL)
240+
} else if path.hasSuffix(".mobileprovision") {
241+
provUrls.append(fileURL)
242+
}
184243
}
185244
}
186245
}
187-
try FileManager.default.removeItem(at: tempDir)
188246
guard p12Urls.count == 1, provUrls.count == 1 else {
189247
throw NSError(domain: "Extraction", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unable to extract certificate"])
190248
}
191249
let p12Url = p12Urls[0]
192250
let provUrl = provUrls[0]
193-
let p12Data = try Data(contentsOf: p12Url)
194-
let provData = try Data(contentsOf: provUrl)
251+
let p12DataLocal = try Data(contentsOf: p12Url)
252+
let provDataLocal = try Data(contentsOf: provUrl)
195253
var successPw: String?
196-
for pw in ["Hydrogen", "Sideloadingdotorg"] {
197-
switch CertificatesManager.check(p12Data: p12Data, password: pw, mobileProvisionData: provData) {
254+
for pwCandidate in ["Hydrogen", "Sideloadingdotorg"] {
255+
switch CertificatesManager.check(p12Data: p12DataLocal, password: pwCandidate, mobileProvisionData: provDataLocal) {
198256
case .success(.success):
199-
successPw = pw
257+
successPw = pwCandidate
200258
break
201259
default:
202260
break
@@ -205,11 +263,11 @@ struct OfficialCertificatesView: View {
205263
guard let pw = successPw else {
206264
throw NSError(domain: "Password", code: 1, userInfo: [NSLocalizedDescriptionKey: "Password check failed"])
207265
}
208-
let exp = ProStoreTools.getExpirationDate(provData: provData)
209-
let dispName = CertificatesManager.getCertificateName(mobileProvisionData: provData) ?? cleanName(release.name)
266+
let exp = ProStoreTools.getExpirationDate(provData: provDataLocal)
267+
let dispName = CertificatesManager.getCertificateName(mobileProvisionData: provDataLocal) ?? cleanName(release.name)
210268
await MainActor.run {
211-
self.p12Data = p12Data
212-
self.provData = provData
269+
self.p12Data = p12DataLocal
270+
self.provData = provDataLocal
213271
self.password = pw
214272
self.displayName = dispName
215273
self.expiry = exp
@@ -226,14 +284,15 @@ struct OfficialCertificatesView: View {
226284
}
227285

228286
private func addCertificate() {
229-
guard let p12Data = p12Data,
230-
let provData = provData,
231-
let pw = password else { return }
287+
guard let p12DataLocal = p12Data,
288+
let provDataLocal = provData,
289+
let pw = password,
290+
!displayName.isEmpty else { return }
232291
isChecking = true
233292
statusMessage = "Adding..."
234293
Task {
235294
do {
236-
_ = try CertificateFileManager.shared.saveCertificate(p12Data: p12Data, provData: provData, password: pw, displayName: displayName)
295+
_ = try CertificateFileManager.shared.saveCertificate(p12Data: p12DataLocal, provData: provDataLocal, password: pw, displayName: displayName)
237296
await MainActor.run {
238297
self.statusMessage = "Added successfully"
239298
self.isChecking = false

0 commit comments

Comments
 (0)