Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Application/DevLogApp/Sources/Resource/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
<false/>
<key>FIRESTORE_DATABASE_ID</key>
<string>$(FIRESTORE_DATABASE_ID)</string>
<key>FUNCTION_API_BASE_URL</key>
<string>$(FUNCTION_API_BASE_URL)</string>
<key>GIDClientID</key>
<string>$(CLIENT_ID)</string>
<key>GITHUB_CLIENT_ID</key>
Expand Down
1 change: 1 addition & 0 deletions Application/DevLogData/Sources/Mapper/WebPageMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public extension WebPageResponse {
imageURL = nil
}
return WebPage(
id: id,
title: title,
url: url,
displayURL: displayURL,
Expand Down
4 changes: 2 additions & 2 deletions Application/DevLogData/Sources/Protocol/WebPageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ import Foundation
public protocol WebPageService {
func fetchWebPages(_ query: String) async throws -> [WebPageResponse]
func upsertWebPage(_ request: WebPageRequest) async throws
func deleteWebPage(_ urlString: String) async throws
func undoDeleteWebPage(_ urlString: String) async throws
func deleteWebPage(_ id: String) async throws
func undoDeleteWebPage(_ id: String) async throws
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,18 @@ final class WebPageRepositoryImpl: WebPageRepository {
}
}

func delete(_ urlString: String) async throws {
func delete(id: String, urlString: String) async throws {
do {
try await webPageService.deleteWebPage(urlString)
try await webPageService.deleteWebPage(id)
await metadataService.removeCachedImage(for: urlString)
} catch {
throw error.toDomain()
}
}

func undoDelete(_ urlString: String) async throws {
func undoDelete(_ id: String) async throws {
do {
try await webPageService.undoDeleteWebPage(urlString)
try await webPageService.undoDeleteWebPage(id)
} catch {
throw error.toDomain()
}
Expand Down
3 changes: 3 additions & 0 deletions Application/DevLogDomain/Sources/Entity/WebPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
import Foundation

public struct WebPage: Hashable {
public let id: String
public let title: String?
public let url: URL
public let displayURL: URL
public let imageURL: URL?

public init(
id: String,
title: String?,
url: URL,
displayURL: URL,
imageURL: URL?
) {
self.id = id
self.title = title
self.url = url
self.displayURL = displayURL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
public protocol WebPageRepository {
func fetch(_ query: String) async throws -> [WebPage]
func upsert(_ urlString: String) async throws
func delete(_ urlString: String) async throws
func undoDelete(_ urlString: String) async throws
func delete(id: String, urlString: String) async throws
func undoDelete(_ id: String) async throws
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
//

public protocol DeleteWebPageUseCase {
func execute(_ urlString: String) async throws
func execute(id: String, urlString: String) async throws
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public final class DeleteWebPageUseCaseImpl: DeleteWebPageUseCase {
self.repository = repository
}

public func execute(_ urlString: String) async throws {
try await repository.delete(urlString)
public func execute(id: String, urlString: String) async throws {
try await repository.delete(id: id, urlString: urlString)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
//

public protocol UndoDeleteWebPageUseCase {
func execute(_ urlString: String) async throws
func execute(_ id: String) async throws
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public final class UndoDeleteWebPageUseCaseImpl: UndoDeleteWebPageUseCase {
self.repository = repository
}

public func execute(_ urlString: String) async throws {
try await repository.undoDelete(urlString)
public func execute(_ id: String) async throws {
try await repository.undoDelete(id)
}
}
30 changes: 27 additions & 3 deletions Application/DevLogInfra/Sources/Common/FirebaseConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
//

import FirebaseFirestore
import FirebaseFunctions
import Foundation

enum FirebaseConfiguration {
private enum InfoKey {
static let databaseID = "FIRESTORE_DATABASE_ID"
static let functionAPIBaseURL = "FUNCTION_API_BASE_URL"
}

static let defaultDatabaseID = "staging"
Expand Down Expand Up @@ -39,7 +39,31 @@ enum FirebaseConfiguration {
Firestore.firestore(database: databaseID)
}

static var functions: Functions {
Functions.functions(region: "asia-northeast3")
static func functionAPIBaseURL() throws -> URL {
if let value = resolvedValue(for: InfoKey.functionAPIBaseURL),
let url = URL(string: value) {
return url
}

throw URLError(.badURL)
}

private static func resolvedValue(for key: String) -> String? {
let environmentValue = ProcessInfo.processInfo.environment[key]?
.trimmingCharacters(in: .whitespacesAndNewlines)
if let environmentValue, !environmentValue.isEmpty {
return environmentValue
}

guard let rawValue = Bundle.main.object(forInfoDictionaryKey: key) as? String else {
return nil
}

let value = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !value.isEmpty, !value.hasPrefix("$(") else {
return nil
}

return value
}
}
14 changes: 0 additions & 14 deletions Application/DevLogInfra/Sources/Extension/FirebaseFunctions+.swift

This file was deleted.

158 changes: 158 additions & 0 deletions Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//
// FunctionAPIClient.swift
// DevLogInfra
//
// Created by opfic on 6/26/26.
//

import FirebaseAuth
import Foundation
import DevLogData
import Nexa

final class FunctionAPIClient {
static let shared = FunctionAPIClient()

private let apiClient: Result<NXAPIClient, Error>

private init() {
let authTokenProvider = FirebaseAuthTokenProvider()
apiClient = Result {
try NXAPIClient(
configuration: NXClientConfiguration(
baseURL: FirebaseConfiguration.functionAPIBaseURL(),
headers: ["Accept": "application/json"],
serverErrorDecoder: FunctionAPIServerErrorDecoder(),
authTokenProvider: authTokenProvider
)
)
}
}

func send(
_ endpoint: FunctionAPIEndpoint<EmptyAPIResponse>,
payload: some Encodable,
requiresAuthentication: Bool = true
) async throws {
var request = try client()
.request(endpoint)
.json(payload)

if requiresAuthentication {
request = request.authorized()
}

_ = try await request.raw()
}

func send(
_ endpoint: FunctionAPIEndpoint<EmptyAPIResponse>,
requiresAuthentication: Bool = true
) async throws {
var request = try client()
.request(endpoint)

if requiresAuthentication {
request = request.authorized()
}

_ = try await request.raw()
}

func send<Response: Decodable>(
_ endpoint: FunctionAPIEndpoint<Response>,
payload: some Encodable,
requiresAuthentication: Bool = true
) async throws -> Response {
var request = try client()
.request(endpoint)
.json(payload)

if requiresAuthentication {
request = request.authorized()
}

return try await request.send()
}

func send<Response: Decodable>(
_ endpoint: FunctionAPIEndpoint<Response>,
requiresAuthentication: Bool = true
) async throws -> Response {
try await send(
endpoint,
payload: EmptyPayload(),
requiresAuthentication: requiresAuthentication
)
}

private func client() throws -> NXAPIClient {
try apiClient.get()
}
Comment thread
opficdev marked this conversation as resolved.
}

struct FunctionAPIEndpoint<Response: Decodable>: NXEndpoint {
let method: NXHTTPMethod
let path: String
}

struct FunctionAPIResponse: Decodable {
let accessToken: String?
let customToken: String?
let refreshToken: String?
let token: String?
}

struct EmptyAPIResponse: Decodable {}

private struct EmptyPayload: Encodable {}

private struct FunctionAPIErrorBody: Decodable {
let code: String
let message: String?
}

private struct FunctionAPIServerErrorDecoder: NXServerErrorDecoder {
func decodeServerError(
data: Data,
response: HTTPURLResponse,
decoder: JSONDecoder
) -> (any Error)? {
guard let body = try? decoder.decode(
FunctionAPIErrorBody.self,
from: data
) else { return nil }

switch body.code {
case EmailFetchError.emailNotFound.code:
return EmailFetchError.emailNotFound
case EmailFetchError.emailMismatch.code:
return EmailFetchError.emailMismatch
default:
return nil
}
}
}

private actor FirebaseAuthTokenProvider: NXAuthTokenProvider {
func currentAccessToken() async throws -> String? {
try await Auth.auth().currentUser?.getIDToken()
}

func refreshAccessToken() async throws -> String? {
try await Auth.auth().currentUser?.getIDToken(forcingRefresh: true)
}
}

extension Error {
var apiEmailFetchError: EmailFetchError? {
guard let error = self as? NXError,
case let .server(
statusCode: _,
data: _,
underlying: underlying
) = error else { return nil }

return underlying as? EmailFetchError
}
}
50 changes: 50 additions & 0 deletions Application/DevLogInfra/Sources/Service/FunctionAPIEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// FunctionAPIEndpoint.swift
// DevLogInfra
//
// Created by opfic on 6/26/26.
//

import Foundation

extension FunctionAPIEndpoint where Response == EmptyAPIResponse {
static func requestTodoDeletion(_ id: String) -> Self {
Self(method: .post, path: "/todos/\(functionAPIPathSegment(id))/deletion-request")
}

static func undoTodoDeletion(_ id: String) -> Self {
Self(method: .delete, path: "/todos/\(functionAPIPathSegment(id))/deletion-request")
}

static func requestWebPageDeletion(_ id: String) -> Self {
Self(method: .post, path: "/web-pages/\(functionAPIPathSegment(id))/deletion-request")
}

static func undoWebPageDeletion(_ id: String) -> Self {
Self(method: .delete, path: "/web-pages/\(functionAPIPathSegment(id))/deletion-request")
}

static func requestPushNotificationDeletion(_ id: String) -> Self {
Self(method: .post, path: "/push-notifications/\(functionAPIPathSegment(id))/deletion-request")
}

static func undoPushNotificationDeletion(_ id: String) -> Self {
Self(method: .delete, path: "/push-notifications/\(functionAPIPathSegment(id))/deletion-request")
}

static let revokeAppleAccessToken = Self(method: .delete, path: "/auth/apple/access-token")
static let revokeGithubAccessToken = Self(method: .delete, path: "/auth/github/access-token")
}

extension FunctionAPIEndpoint where Response == FunctionAPIResponse {
static let requestAppleCustomToken = Self(method: .post, path: "/auth/apple/custom-token")
static let refreshAppleAccessToken = Self(method: .post, path: "/auth/apple/access-token")
static let requestAppleRefreshToken = Self(method: .post, path: "/auth/apple/refresh-token")
static let requestGithubTokens = Self(method: .post, path: "/auth/github/tokens")
}

private func functionAPIPathSegment(_ value: String) -> String {
var allowed = CharacterSet.alphanumerics
allowed.insert(charactersIn: "-._~")
return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value
}
Loading
Loading