fix(core): fix ios blob upload (#10263)
This commit is contained in:
@@ -22,7 +22,7 @@ export class CloudAwarenessStorage extends AwarenessStorageBase {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
connection = new SocketConnection(`${this.options.serverBaseUrl}/`);
|
connection = new SocketConnection(this.options.serverBaseUrl);
|
||||||
|
|
||||||
private get socket() {
|
private get socket() {
|
||||||
return this.connection.inner.socket;
|
return this.connection.inner.socket;
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ class CloudDocStorageConnection extends SocketConnection {
|
|||||||
private readonly options: CloudDocStorageOptions,
|
private readonly options: CloudDocStorageOptions,
|
||||||
private readonly onServerUpdate: ServerEventsMap['space:broadcast-doc-update']
|
private readonly onServerUpdate: ServerEventsMap['space:broadcast-doc-update']
|
||||||
) {
|
) {
|
||||||
super(`${options.serverBaseUrl}/`);
|
super(options.serverBaseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
idConverter: IdConverter | null = null;
|
idConverter: IdConverter | null = null;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export * from './awareness';
|
|||||||
export * from './blob';
|
export * from './blob';
|
||||||
export * from './doc';
|
export * from './doc';
|
||||||
export * from './doc-static';
|
export * from './doc-static';
|
||||||
|
export * from './socket';
|
||||||
|
|
||||||
export const cloudStorages = [
|
export const cloudStorages = [
|
||||||
CloudDocStorage,
|
CloudDocStorage,
|
||||||
|
|||||||
@@ -150,6 +150,16 @@ export function base64ToUint8Array(base64: string) {
|
|||||||
return new Uint8Array(binaryArray);
|
return new Uint8Array(binaryArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let authMethod:
|
||||||
|
| ((endpoint: string, cb: (data: object) => void) => void)
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
export function configureSocketAuthMethod(
|
||||||
|
cb: (endpoint: string, cb: (data: object) => void) => void
|
||||||
|
) {
|
||||||
|
authMethod = cb;
|
||||||
|
}
|
||||||
|
|
||||||
class SocketManager {
|
class SocketManager {
|
||||||
private readonly socketIOManager: SocketIOManager;
|
private readonly socketIOManager: SocketIOManager;
|
||||||
socket: Socket;
|
socket: Socket;
|
||||||
@@ -158,12 +168,20 @@ class SocketManager {
|
|||||||
constructor(endpoint: string) {
|
constructor(endpoint: string) {
|
||||||
this.socketIOManager = new SocketIOManager(endpoint, {
|
this.socketIOManager = new SocketIOManager(endpoint, {
|
||||||
autoConnect: false,
|
autoConnect: false,
|
||||||
transports: [BUILD_CONFIG.isIOS ? 'polling' : 'websocket'],
|
transports: ['websocket'],
|
||||||
secure: new URL(endpoint).protocol === 'https:',
|
secure: new URL(endpoint).protocol === 'https:',
|
||||||
// we will handle reconnection by ourselves
|
// we will handle reconnection by ourselves
|
||||||
reconnection: false,
|
reconnection: false,
|
||||||
});
|
});
|
||||||
this.socket = this.socketIOManager.socket('/');
|
this.socket = this.socketIOManager.socket('/', {
|
||||||
|
auth(cb) {
|
||||||
|
if (authMethod) {
|
||||||
|
authMethod(endpoint, cb);
|
||||||
|
} else {
|
||||||
|
cb({});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
50A285D82D112A5E000D5A6D /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */; };
|
50A285D82D112A5E000D5A6D /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */; };
|
||||||
50A285DC2D112B24000D5A6D /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50A285DB2D112B24000D5A6D /* Intelligents */; };
|
50A285DC2D112B24000D5A6D /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50A285DB2D112B24000D5A6D /* Intelligents */; };
|
||||||
9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */; };
|
9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */; };
|
||||||
|
9D5622962D64A6A5009F1BE4 /* AuthPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D5622952D64A6A4009F1BE4 /* AuthPlugin.swift */; };
|
||||||
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */; };
|
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */; };
|
||||||
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE172CCB9876006677DB /* CookieManager.swift */; };
|
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE172CCB9876006677DB /* CookieManager.swift */; };
|
||||||
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; };
|
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; };
|
||||||
@@ -24,7 +25,6 @@
|
|||||||
9D90BE2B2CCB9876006677DB /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1F2CCB9876006677DB /* config.xml */; };
|
9D90BE2B2CCB9876006677DB /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1F2CCB9876006677DB /* config.xml */; };
|
||||||
9D90BE2D2CCB9876006677DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE222CCB9876006677DB /* Main.storyboard */; };
|
9D90BE2D2CCB9876006677DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE222CCB9876006677DB /* Main.storyboard */; };
|
||||||
9D90BE2E2CCB9876006677DB /* public in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE232CCB9876006677DB /* public */; };
|
9D90BE2E2CCB9876006677DB /* public in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE232CCB9876006677DB /* public */; };
|
||||||
9DEC593B2D3002E70027CEBD /* AffineHttpHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC593A2D3002C70027CEBD /* AffineHttpHandler.swift */; };
|
|
||||||
9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC59422D323EE00027CEBD /* Mutex.swift */; };
|
9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC59422D323EE00027CEBD /* Mutex.swift */; };
|
||||||
9DFCD1462D27D1D70028C92B /* libaffine_mobile_native.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */; };
|
9DFCD1462D27D1D70028C92B /* libaffine_mobile_native.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */; };
|
||||||
C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
||||||
50A285D62D112A5E000D5A6D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
50A285D62D112A5E000D5A6D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||||
9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSValueContainerExt.swift; sourceTree = "<group>"; };
|
9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSValueContainerExt.swift; sourceTree = "<group>"; };
|
||||||
|
9D5622952D64A6A4009F1BE4 /* AuthPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthPlugin.swift; sourceTree = "<group>"; };
|
||||||
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashcashPlugin.swift; sourceTree = "<group>"; };
|
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashcashPlugin.swift; sourceTree = "<group>"; };
|
||||||
9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = "<group>"; };
|
9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = "<group>"; };
|
||||||
9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = "<group>"; };
|
9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = "<group>"; };
|
||||||
@@ -54,7 +55,6 @@
|
|||||||
9D90BE202CCB9876006677DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
9D90BE202CCB9876006677DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
9D90BE212CCB9876006677DB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
9D90BE212CCB9876006677DB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
9D90BE232CCB9876006677DB /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
9D90BE232CCB9876006677DB /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||||
9DEC593A2D3002C70027CEBD /* AffineHttpHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffineHttpHandler.swift; sourceTree = "<group>"; };
|
|
||||||
9DEC59422D323EE00027CEBD /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = "<group>"; };
|
9DEC59422D323EE00027CEBD /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = "<group>"; };
|
||||||
9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libaffine_mobile_native.a; sourceTree = "<group>"; };
|
9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libaffine_mobile_native.a; sourceTree = "<group>"; };
|
||||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -136,6 +136,14 @@
|
|||||||
name = Pods;
|
name = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
9D5622942D64A69C009F1BE4 /* Auth */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9D5622952D64A6A4009F1BE4 /* AuthPlugin.swift */,
|
||||||
|
);
|
||||||
|
path = Auth;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
9D90BE192CCB9876006677DB /* Cookie */ = {
|
9D90BE192CCB9876006677DB /* Cookie */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -150,6 +158,7 @@
|
|||||||
9D90BE1A2CCB9876006677DB /* Plugins */ = {
|
9D90BE1A2CCB9876006677DB /* Plugins */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
9D5622942D64A69C009F1BE4 /* Auth */,
|
||||||
C45499AB2D140B5000E21978 /* NBStore */,
|
C45499AB2D140B5000E21978 /* NBStore */,
|
||||||
E93B276A2CED9298001409B8 /* NavigationGesture */,
|
E93B276A2CED9298001409B8 /* NavigationGesture */,
|
||||||
9D90BE192CCB9876006677DB /* Cookie */,
|
9D90BE192CCB9876006677DB /* Cookie */,
|
||||||
@@ -161,7 +170,6 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
9DEC59422D323EE00027CEBD /* Mutex.swift */,
|
9DEC59422D323EE00027CEBD /* Mutex.swift */,
|
||||||
9DEC593A2D3002C70027CEBD /* AffineHttpHandler.swift */,
|
|
||||||
9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */,
|
9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */,
|
||||||
9D90BE1A2CCB9876006677DB /* Plugins */,
|
9D90BE1A2CCB9876006677DB /* Plugins */,
|
||||||
9D90BE1C2CCB9876006677DB /* AppDelegate.swift */,
|
9D90BE1C2CCB9876006677DB /* AppDelegate.swift */,
|
||||||
@@ -337,9 +345,9 @@
|
|||||||
9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */,
|
9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */,
|
||||||
5075136E2D1925BC00AD60C0 /* IntelligentsPlugin.swift in Sources */,
|
5075136E2D1925BC00AD60C0 /* IntelligentsPlugin.swift in Sources */,
|
||||||
5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */,
|
5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */,
|
||||||
9DEC593B2D3002E70027CEBD /* AffineHttpHandler.swift in Sources */,
|
|
||||||
C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */,
|
C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */,
|
||||||
C4C97C7D2D030BE000BC2AD1 /* affine_mobile_nativeFFI.h in Sources */,
|
C4C97C7D2D030BE000BC2AD1 /* affine_mobile_nativeFFI.h in Sources */,
|
||||||
|
9D5622962D64A6A5009F1BE4 /* AuthPlugin.swift in Sources */,
|
||||||
C4C97C7E2D030BE000BC2AD1 /* affine_mobile_nativeFFI.modulemap in Sources */,
|
C4C97C7E2D030BE000BC2AD1 /* affine_mobile_nativeFFI.modulemap in Sources */,
|
||||||
E93B276C2CED92B1001409B8 /* NavigationGesturePlugin.swift in Sources */,
|
E93B276C2CED92B1001409B8 /* NavigationGesturePlugin.swift in Sources */,
|
||||||
9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */,
|
9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */,
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
//
|
|
||||||
// RequestUrlSchemeHandler.swift
|
|
||||||
// App
|
|
||||||
//
|
|
||||||
// Created by EYHN on 2025/1/9.
|
|
||||||
//
|
|
||||||
|
|
||||||
import WebKit
|
|
||||||
|
|
||||||
enum AffineHttpError: Error {
|
|
||||||
case invalidOperation(reason: String), invalidState(reason: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
class AffineHttpHandler: NSObject, WKURLSchemeHandler {
|
|
||||||
func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) {
|
|
||||||
urlSchemeTask.stopped = false
|
|
||||||
guard let rawUrl = urlSchemeTask.request.url else {
|
|
||||||
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad request"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let scheme = rawUrl.scheme else {
|
|
||||||
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad request"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let httpProtocol = scheme == "affine-http" ? "http" : "https"
|
|
||||||
guard let urlComponents = URLComponents(url: rawUrl, resolvingAgainstBaseURL: true) else {
|
|
||||||
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad request"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let host = urlComponents.host else {
|
|
||||||
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad url"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let path = urlComponents.path
|
|
||||||
let query = urlComponents.query != nil ? "?\(urlComponents.query!)" : ""
|
|
||||||
let port = urlComponents.port != nil ? ":\(urlComponents.port!)" : ""
|
|
||||||
guard let targetUrl = URL(string: "\(httpProtocol)://\(host)\(port)\(path)\(query)") else {
|
|
||||||
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad url"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = URLRequest(url: targetUrl);
|
|
||||||
request.httpMethod = urlSchemeTask.request.httpMethod;
|
|
||||||
request.httpShouldHandleCookies = true
|
|
||||||
request.httpBody = urlSchemeTask.request.httpBody
|
|
||||||
urlSchemeTask.request.allHTTPHeaderFields?.filter({
|
|
||||||
key, value in
|
|
||||||
let normalizedKey = key.lowercased()
|
|
||||||
return normalizedKey == "content-type" ||
|
|
||||||
normalizedKey == "content-length" ||
|
|
||||||
normalizedKey == "accept"
|
|
||||||
}).forEach {
|
|
||||||
key, value in
|
|
||||||
request.setValue(value, forHTTPHeaderField: key)
|
|
||||||
}
|
|
||||||
|
|
||||||
let task = URLSession.shared.dataTask(with: request) {
|
|
||||||
(rawData, rawResponse, error) in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if urlSchemeTask.stopped {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if error != nil {
|
|
||||||
urlSchemeTask.didFailWithError(error!)
|
|
||||||
} else {
|
|
||||||
guard let httpResponse = rawResponse as? HTTPURLResponse else {
|
|
||||||
urlSchemeTask.didFailWithError(AffineHttpError.invalidState(reason: "bad response"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let inheritedHeaders = httpResponse.allHeaderFields.filter({
|
|
||||||
key, value in
|
|
||||||
let normalizedKey = (key as? String)?.lowercased()
|
|
||||||
return normalizedKey == "content-type" ||
|
|
||||||
normalizedKey == "content-length"
|
|
||||||
}) as? [String: String] ?? [:]
|
|
||||||
let newHeaders: [String: String] = [
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
"Access-Control-Allow-Methods": "*"
|
|
||||||
]
|
|
||||||
|
|
||||||
guard let response = HTTPURLResponse.init(url: rawUrl, statusCode: httpResponse.statusCode, httpVersion: nil, headerFields: inheritedHeaders.merging(newHeaders, uniquingKeysWith: { (_, newHeaders) in newHeaders })) else {
|
|
||||||
urlSchemeTask.didFailWithError(AffineHttpError.invalidState(reason: "failed to create response"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
urlSchemeTask.didReceive(response)
|
|
||||||
if rawData != nil {
|
|
||||||
urlSchemeTask.didReceive(rawData!)
|
|
||||||
}
|
|
||||||
urlSchemeTask.didFinish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
task.resume()
|
|
||||||
|
|
||||||
urlSchemeTask.dataTask = task
|
|
||||||
}
|
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
|
|
||||||
urlSchemeTask.stopped = true
|
|
||||||
urlSchemeTask.dataTask?.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension WKURLSchemeTask {
|
|
||||||
var stopped: Bool {
|
|
||||||
get {
|
|
||||||
return objc_getAssociatedObject(self, &stoppedKey) as? Bool ?? false
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
objc_setAssociatedObject(self, &stoppedKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var dataTask: URLSessionDataTask? {
|
|
||||||
get {
|
|
||||||
return objc_getAssociatedObject(self, &dataTaskKey) as? URLSessionDataTask
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
objc_setAssociatedObject(self, &dataTaskKey, newValue, .OBJC_ASSOCIATION_RETAIN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var stoppedKey = malloc(1)
|
|
||||||
private var dataTaskKey = malloc(1)
|
|
||||||
@@ -20,13 +20,12 @@ class AFFiNEViewController: CAPBridgeViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func webView(with frame: CGRect, configuration: WKWebViewConfiguration) -> WKWebView {
|
override func webView(with frame: CGRect, configuration: WKWebViewConfiguration) -> WKWebView {
|
||||||
configuration.setURLSchemeHandler(AffineHttpHandler(), forURLScheme: "affine-http")
|
|
||||||
configuration.setURLSchemeHandler(AffineHttpHandler(), forURLScheme: "affine-https")
|
|
||||||
return super.webView(with: frame, configuration: configuration)
|
return super.webView(with: frame, configuration: configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func capacitorDidLoad() {
|
override func capacitorDidLoad() {
|
||||||
let plugins: [CAPPlugin] = [
|
let plugins: [CAPPlugin] = [
|
||||||
|
AuthPlugin(),
|
||||||
CookiePlugin(),
|
CookiePlugin(),
|
||||||
HashcashPlugin(),
|
HashcashPlugin(),
|
||||||
NavigationGesturePlugin(),
|
NavigationGesturePlugin(),
|
||||||
|
|||||||
170
packages/frontend/apps/ios/App/App/Plugins/Auth/AuthPlugin.swift
Normal file
170
packages/frontend/apps/ios/App/App/Plugins/Auth/AuthPlugin.swift
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import Capacitor
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||||
|
public let identifier = "AuthPlugin"
|
||||||
|
public let jsName = "Auth"
|
||||||
|
public let pluginMethods: [CAPPluginMethod] = [
|
||||||
|
CAPPluginMethod(name: "signInMagicLink", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "signInOauth", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "signInPassword", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "signOut", returnType: CAPPluginReturnPromise),
|
||||||
|
]
|
||||||
|
|
||||||
|
@objc public func signInMagicLink(_ call: CAPPluginCall) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let endpoint = try call.getStringEnsure("endpoint")
|
||||||
|
let email = try call.getStringEnsure("email")
|
||||||
|
let token = try call.getStringEnsure("token")
|
||||||
|
|
||||||
|
let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/auth/magic-link", headers: [:], body: ["email": email, "token": token])
|
||||||
|
|
||||||
|
if response.statusCode != 200 {
|
||||||
|
if let textBody = String(data: data, encoding: .utf8) {
|
||||||
|
call.reject(textBody)
|
||||||
|
} else {
|
||||||
|
call.reject("Failed to sign in")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let token = try self.tokenFromCookie(endpoint) else {
|
||||||
|
call.reject("token not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
call.resolve(["token": token])
|
||||||
|
} catch {
|
||||||
|
call.reject("Failed to sign in, \(error)", nil, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func signInOauth(_ call: CAPPluginCall) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let endpoint = try call.getStringEnsure("endpoint")
|
||||||
|
let code = try call.getStringEnsure("code")
|
||||||
|
let state = try call.getStringEnsure("state")
|
||||||
|
|
||||||
|
let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/oauth/callback", headers: [:], body: ["code": code, "state": state])
|
||||||
|
|
||||||
|
if response.statusCode != 200 {
|
||||||
|
if let textBody = String(data: data, encoding: .utf8) {
|
||||||
|
call.reject(textBody)
|
||||||
|
} else {
|
||||||
|
call.reject("Failed to sign in")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let token = try self.tokenFromCookie(endpoint) else {
|
||||||
|
call.reject("token not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
call.resolve(["token": token])
|
||||||
|
} catch {
|
||||||
|
call.reject("Failed to sign in, \(error)", nil, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func signInPassword(_ call: CAPPluginCall) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let endpoint = try call.getStringEnsure("endpoint")
|
||||||
|
let email = try call.getStringEnsure("email")
|
||||||
|
let password = try call.getStringEnsure("password")
|
||||||
|
let verifyToken = call.getString("verifyToken")
|
||||||
|
let challenge = call.getString("challenge")
|
||||||
|
|
||||||
|
let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/auth/sign-in", headers: [
|
||||||
|
"x-captcha-token": verifyToken,
|
||||||
|
"x-captcha-challenge": challenge,
|
||||||
|
], body: ["email": email, "password": password])
|
||||||
|
|
||||||
|
if response.statusCode != 200 {
|
||||||
|
if let textBody = String(data: data, encoding: .utf8) {
|
||||||
|
call.reject(textBody)
|
||||||
|
} else {
|
||||||
|
call.reject("Failed to sign in")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let token = try self.tokenFromCookie(endpoint) else {
|
||||||
|
call.reject("token not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
call.resolve(["token": token])
|
||||||
|
} catch {
|
||||||
|
call.reject("Failed to sign in, \(error)", nil, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func signOut(_ call: CAPPluginCall) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let endpoint = try call.getStringEnsure("endpoint")
|
||||||
|
|
||||||
|
let (data, response) = try await self.fetch(endpoint, method: "GET", action: "/api/auth/sign-out", headers: [:], body: nil)
|
||||||
|
|
||||||
|
if response.statusCode != 200 {
|
||||||
|
if let textBody = String(data: data, encoding: .utf8) {
|
||||||
|
call.reject(textBody)
|
||||||
|
} else {
|
||||||
|
call.reject("Failed to sign in")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
call.resolve(["ok": true])
|
||||||
|
} catch {
|
||||||
|
call.reject("Failed to sign in, \(error)", nil, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func tokenFromCookie(_ endpoint: String) throws -> String? {
|
||||||
|
guard let endpointUrl = URL(string: endpoint) else {
|
||||||
|
throw AuthError.invalidEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
if let cookie = HTTPCookieStorage.shared.cookies(for: endpointUrl)?.first(where: {
|
||||||
|
$0.name == "affine_session"
|
||||||
|
}) {
|
||||||
|
return cookie.value
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetch(_ endpoint: String, method: String, action: String, headers: Dictionary<String, String?>, body: Encodable?) async throws -> (Data, HTTPURLResponse) {
|
||||||
|
guard let targetUrl = URL(string: "\(endpoint)\(action)") else {
|
||||||
|
throw AuthError.invalidEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: targetUrl);
|
||||||
|
request.httpMethod = method;
|
||||||
|
request.httpShouldHandleCookies = true
|
||||||
|
for (key, value) in headers {
|
||||||
|
request.setValue(value, forHTTPHeaderField: key)
|
||||||
|
}
|
||||||
|
if body != nil {
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.httpBody = try JSONEncoder().encode(body!)
|
||||||
|
}
|
||||||
|
request.timeoutInterval = 10 // time out 10s
|
||||||
|
|
||||||
|
let (data, response) = try await URLSession.shared.data(for: request);
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
throw AuthError.internalError
|
||||||
|
}
|
||||||
|
return (data, httpResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthError: Error {
|
||||||
|
case invalidEndpoint, internalError
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"@sentry/react": "^8.44.0",
|
"@sentry/react": "^8.44.0",
|
||||||
"@toeverything/infra": "workspace:^",
|
"@toeverything/infra": "workspace:^",
|
||||||
"async-call-rpc": "^6.4.2",
|
"async-call-rpc": "^6.4.2",
|
||||||
|
"idb": "^8.0.0",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import { router } from '@affine/core/mobile/router';
|
|||||||
import { configureCommonModules } from '@affine/core/modules';
|
import { configureCommonModules } from '@affine/core/modules';
|
||||||
import { AIButtonProvider } from '@affine/core/modules/ai-button';
|
import { AIButtonProvider } from '@affine/core/modules/ai-button';
|
||||||
import {
|
import {
|
||||||
|
AuthProvider,
|
||||||
AuthService,
|
AuthService,
|
||||||
DefaultServerService,
|
DefaultServerService,
|
||||||
|
ServerScope,
|
||||||
|
ServerService,
|
||||||
ServersService,
|
ServersService,
|
||||||
ValidatorProvider,
|
ValidatorProvider,
|
||||||
} from '@affine/core/modules/cloud';
|
} from '@affine/core/modules/cloud';
|
||||||
@@ -51,9 +54,11 @@ import { RouterProvider } from 'react-router-dom';
|
|||||||
|
|
||||||
import { BlocksuiteMenuConfigProvider } from './bs-menu-config';
|
import { BlocksuiteMenuConfigProvider } from './bs-menu-config';
|
||||||
import { ModalConfigProvider } from './modal-config';
|
import { ModalConfigProvider } from './modal-config';
|
||||||
|
import { Auth } from './plugins/auth';
|
||||||
import { Hashcash } from './plugins/hashcash';
|
import { Hashcash } from './plugins/hashcash';
|
||||||
import { Intelligents } from './plugins/intelligents';
|
import { Intelligents } from './plugins/intelligents';
|
||||||
import { NbStoreNativeDBApis } from './plugins/nbstore';
|
import { NbStoreNativeDBApis } from './plugins/nbstore';
|
||||||
|
import { writeEndpointToken } from './proxy';
|
||||||
import { enableNavigationGesture$ } from './web-navigation-control';
|
import { enableNavigationGesture$ } from './web-navigation-control';
|
||||||
|
|
||||||
const storeManagerClient = createStoreManagerClient();
|
const storeManagerClient = createStoreManagerClient();
|
||||||
@@ -134,6 +139,41 @@ framework.impl(AIButtonProvider, {
|
|||||||
return Intelligents.dismissIntelligentsButton();
|
return Intelligents.dismissIntelligentsButton();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
framework.scope(ServerScope).override(AuthProvider, resolver => {
|
||||||
|
const serverService = resolver.get(ServerService);
|
||||||
|
const endpoint = serverService.server.baseUrl;
|
||||||
|
return {
|
||||||
|
async signInMagicLink(email, linkToken) {
|
||||||
|
const { token } = await Auth.signInMagicLink({
|
||||||
|
endpoint,
|
||||||
|
email,
|
||||||
|
token: linkToken,
|
||||||
|
});
|
||||||
|
await writeEndpointToken(endpoint, token);
|
||||||
|
},
|
||||||
|
async signInOauth(code, state, _provider) {
|
||||||
|
const { token } = await Auth.signInOauth({
|
||||||
|
endpoint,
|
||||||
|
code,
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
await writeEndpointToken(endpoint, token);
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
async signInPassword(credential) {
|
||||||
|
const { token } = await Auth.signInPassword({
|
||||||
|
endpoint,
|
||||||
|
...credential,
|
||||||
|
});
|
||||||
|
await writeEndpointToken(endpoint, token);
|
||||||
|
},
|
||||||
|
async signOut() {
|
||||||
|
await Auth.signOut({
|
||||||
|
endpoint,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const frameworkProvider = framework.provider();
|
const frameworkProvider = framework.provider();
|
||||||
|
|
||||||
|
|||||||
20
packages/frontend/apps/ios/src/plugins/auth/definitions.ts
Normal file
20
packages/frontend/apps/ios/src/plugins/auth/definitions.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export interface AuthPlugin {
|
||||||
|
signInMagicLink(options: {
|
||||||
|
endpoint: string;
|
||||||
|
email: string;
|
||||||
|
token: string;
|
||||||
|
}): Promise<{ token: string }>;
|
||||||
|
signInOauth(options: {
|
||||||
|
endpoint: string;
|
||||||
|
code: string;
|
||||||
|
state: string;
|
||||||
|
}): Promise<{ token: string }>;
|
||||||
|
signInPassword(options: {
|
||||||
|
endpoint: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
verifyToken?: string;
|
||||||
|
challenge?: string;
|
||||||
|
}): Promise<{ token: string }>;
|
||||||
|
signOut(options: { endpoint: string }): Promise<void>;
|
||||||
|
}
|
||||||
8
packages/frontend/apps/ios/src/plugins/auth/index.ts
Normal file
8
packages/frontend/apps/ios/src/plugins/auth/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { registerPlugin } from '@capacitor/core';
|
||||||
|
|
||||||
|
import type { AuthPlugin } from './definitions';
|
||||||
|
|
||||||
|
const Auth = registerPlugin<AuthPlugin>('Auth');
|
||||||
|
|
||||||
|
export * from './definitions';
|
||||||
|
export { Auth };
|
||||||
65
packages/frontend/apps/ios/src/proxy.ts
Normal file
65
packages/frontend/apps/ios/src/proxy.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { openDB } from 'idb';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the below code includes the custom fetch and xmlhttprequest implementation for ios webview.
|
||||||
|
* should be included in the entry file of the app or webworker.
|
||||||
|
*/
|
||||||
|
const rawFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
const request = new Request(input, init);
|
||||||
|
|
||||||
|
const origin = new URL(request.url, globalThis.location.origin).origin;
|
||||||
|
|
||||||
|
const token = await readEndpointToken(origin);
|
||||||
|
if (token) {
|
||||||
|
request.headers.set('Authorization', `Bearer ${token}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawFetch(request);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawXMLHttpRequest = globalThis.XMLHttpRequest;
|
||||||
|
globalThis.XMLHttpRequest = class extends rawXMLHttpRequest {
|
||||||
|
override send(body?: Document | XMLHttpRequestBodyInit | null): void {
|
||||||
|
const origin = new URL(this.responseURL, globalThis.location.origin).origin;
|
||||||
|
|
||||||
|
readEndpointToken(origin).then(
|
||||||
|
token => {
|
||||||
|
if (token) {
|
||||||
|
this.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||||
|
}
|
||||||
|
return super.send(body);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
throw new Error('Failed to read token');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function readEndpointToken(
|
||||||
|
endpoint: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
const idb = await openDB('affine-token', 1, {
|
||||||
|
upgrade(db) {
|
||||||
|
if (!db.objectStoreNames.contains('tokens')) {
|
||||||
|
db.createObjectStore('tokens', { keyPath: 'endpoint' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = await idb.get('tokens', endpoint);
|
||||||
|
return token ? token.token : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeEndpointToken(endpoint: string, token: string) {
|
||||||
|
const db = await openDB('affine-token', 1, {
|
||||||
|
upgrade(db) {
|
||||||
|
if (!db.objectStoreNames.contains('tokens')) {
|
||||||
|
db.createObjectStore('tokens', { keyPath: 'endpoint' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.put('tokens', { endpoint, token });
|
||||||
|
}
|
||||||
@@ -1,62 +1,2 @@
|
|||||||
import '@affine/core/bootstrap/browser';
|
import '@affine/core/bootstrap/browser';
|
||||||
|
import './proxy';
|
||||||
/**
|
|
||||||
* the below code includes the custom fetch and xmlhttprequest implementation for ios webview.
|
|
||||||
* should be included in the entry file of the app or webworker.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* we override the browser's fetch function with our custom fetch function to
|
|
||||||
* overcome the restrictions of cross-domain and third-party cookies in ios webview.
|
|
||||||
*
|
|
||||||
* the custom fetch function will convert the request to `affine-http://` or `affine-https://`
|
|
||||||
* and send the request to the server.
|
|
||||||
*/
|
|
||||||
const rawFetch = globalThis.fetch;
|
|
||||||
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
||||||
let url = new URL(
|
|
||||||
typeof input === 'string'
|
|
||||||
? input
|
|
||||||
: input instanceof URL
|
|
||||||
? input.toString()
|
|
||||||
: input.url,
|
|
||||||
globalThis.location.origin
|
|
||||||
).href;
|
|
||||||
|
|
||||||
if (url.startsWith('capacitor:')) {
|
|
||||||
return rawFetch(input, init);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.startsWith('http:')) {
|
|
||||||
url = 'affine-http:' + url.slice(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.startsWith('https:')) {
|
|
||||||
url = 'affine-https:' + url.slice(6);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawFetch(url, input instanceof Request ? input : init);
|
|
||||||
};
|
|
||||||
|
|
||||||
const rawXMLHttpRequest = globalThis.XMLHttpRequest;
|
|
||||||
globalThis.XMLHttpRequest = class extends rawXMLHttpRequest {
|
|
||||||
override open(
|
|
||||||
method: string,
|
|
||||||
url: string,
|
|
||||||
async?: boolean,
|
|
||||||
user?: string,
|
|
||||||
password?: string
|
|
||||||
) {
|
|
||||||
let normalizedUrl = new URL(url, globalThis.location.origin).href;
|
|
||||||
|
|
||||||
if (normalizedUrl.startsWith('http:')) {
|
|
||||||
url = 'affine-http:' + url.slice(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalizedUrl.startsWith('https:')) {
|
|
||||||
url = 'affine-https:' + url.slice(6);
|
|
||||||
}
|
|
||||||
|
|
||||||
(super.open as any)(method, url, async, user, password);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import './setup';
|
import './setup';
|
||||||
|
|
||||||
import { broadcastChannelStorages } from '@affine/nbstore/broadcast-channel';
|
import { broadcastChannelStorages } from '@affine/nbstore/broadcast-channel';
|
||||||
import { cloudStorages } from '@affine/nbstore/cloud';
|
import {
|
||||||
|
cloudStorages,
|
||||||
|
configureSocketAuthMethod,
|
||||||
|
} from '@affine/nbstore/cloud';
|
||||||
import {
|
import {
|
||||||
bindNativeDBApis,
|
bindNativeDBApis,
|
||||||
type NativeDBApis,
|
type NativeDBApis,
|
||||||
@@ -14,6 +17,18 @@ import {
|
|||||||
import { type MessageCommunicapable, OpConsumer } from '@toeverything/infra/op';
|
import { type MessageCommunicapable, OpConsumer } from '@toeverything/infra/op';
|
||||||
import { AsyncCall } from 'async-call-rpc';
|
import { AsyncCall } from 'async-call-rpc';
|
||||||
|
|
||||||
|
import { readEndpointToken } from './proxy';
|
||||||
|
|
||||||
|
configureSocketAuthMethod((endpoint, cb) => {
|
||||||
|
readEndpointToken(endpoint)
|
||||||
|
.then(token => {
|
||||||
|
cb({ token });
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
globalThis.addEventListener('message', e => {
|
globalThis.addEventListener('message', e => {
|
||||||
if (e.data.type === 'native-db-api-channel') {
|
if (e.data.type === 'native-db-api-channel') {
|
||||||
const port = e.ports[0] as MessagePort;
|
const port = e.ports[0] as MessagePort;
|
||||||
|
|||||||
60
packages/frontend/core/src/modules/cloud/impl/auth.ts
Normal file
60
packages/frontend/core/src/modules/cloud/impl/auth.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { AuthProvider } from '../provider/auth';
|
||||||
|
import { ServerScope } from '../scopes/server';
|
||||||
|
import { FetchService } from '../services/fetch';
|
||||||
|
|
||||||
|
export function configureDefaultAuthProvider(framework: Framework) {
|
||||||
|
framework.scope(ServerScope).override(AuthProvider, resolver => {
|
||||||
|
const fetchService = resolver.get(FetchService);
|
||||||
|
return {
|
||||||
|
async signInMagicLink(email: string, token: string) {
|
||||||
|
await fetchService.fetch('/api/auth/magic-link', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email, token }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async signInOauth(code: string, state: string, _provider: string) {
|
||||||
|
const res = await fetchService.fetch('/api/oauth/callback', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ code, state }),
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return await res.json();
|
||||||
|
},
|
||||||
|
async signInPassword(credential: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
verifyToken?: string;
|
||||||
|
challenge?: string;
|
||||||
|
}) {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (credential.verifyToken) {
|
||||||
|
headers['x-captcha-token'] = credential.verifyToken;
|
||||||
|
}
|
||||||
|
if (credential.challenge) {
|
||||||
|
headers['x-captcha-challenge'] = credential.challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchService.fetch('/api/auth/sign-in', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(credential),
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async signOut() {
|
||||||
|
await fetchService.fetch('/api/auth/sign-out');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -11,7 +11,9 @@ export { AccountChanged } from './events/account-changed';
|
|||||||
export { AccountLoggedIn } from './events/account-logged-in';
|
export { AccountLoggedIn } from './events/account-logged-in';
|
||||||
export { AccountLoggedOut } from './events/account-logged-out';
|
export { AccountLoggedOut } from './events/account-logged-out';
|
||||||
export { ServerInitialized } from './events/server-initialized';
|
export { ServerInitialized } from './events/server-initialized';
|
||||||
|
export { AuthProvider } from './provider/auth';
|
||||||
export { ValidatorProvider } from './provider/validator';
|
export { ValidatorProvider } from './provider/validator';
|
||||||
|
export { ServerScope } from './scopes/server';
|
||||||
export { AcceptInviteService } from './services/accept-invite';
|
export { AcceptInviteService } from './services/accept-invite';
|
||||||
export { AuthService } from './services/auth';
|
export { AuthService } from './services/auth';
|
||||||
export { CaptchaService } from './services/captcha';
|
export { CaptchaService } from './services/captcha';
|
||||||
@@ -51,6 +53,8 @@ import { UserFeature } from './entities/user-feature';
|
|||||||
import { UserQuota } from './entities/user-quota';
|
import { UserQuota } from './entities/user-quota';
|
||||||
import { WorkspaceInvoices } from './entities/workspace-invoices';
|
import { WorkspaceInvoices } from './entities/workspace-invoices';
|
||||||
import { WorkspaceSubscription } from './entities/workspace-subscription';
|
import { WorkspaceSubscription } from './entities/workspace-subscription';
|
||||||
|
import { configureDefaultAuthProvider } from './impl/auth';
|
||||||
|
import { AuthProvider } from './provider/auth';
|
||||||
import { ValidatorProvider } from './provider/validator';
|
import { ValidatorProvider } from './provider/validator';
|
||||||
import { ServerScope } from './scopes/server';
|
import { ServerScope } from './scopes/server';
|
||||||
import { AcceptInviteService } from './services/accept-invite';
|
import { AcceptInviteService } from './services/accept-invite';
|
||||||
@@ -88,6 +92,8 @@ import { UserFeatureStore } from './stores/user-feature';
|
|||||||
import { UserQuotaStore } from './stores/user-quota';
|
import { UserQuotaStore } from './stores/user-quota';
|
||||||
|
|
||||||
export function configureCloudModule(framework: Framework) {
|
export function configureCloudModule(framework: Framework) {
|
||||||
|
configureDefaultAuthProvider(framework);
|
||||||
|
|
||||||
framework
|
framework
|
||||||
.service(ServersService, [ServerListStore, ServerConfigStore])
|
.service(ServersService, [ServerListStore, ServerConfigStore])
|
||||||
.service(DefaultServerService, [ServersService])
|
.service(DefaultServerService, [ServersService])
|
||||||
@@ -112,6 +118,7 @@ export function configureCloudModule(framework: Framework) {
|
|||||||
GraphQLService,
|
GraphQLService,
|
||||||
GlobalState,
|
GlobalState,
|
||||||
ServerService,
|
ServerService,
|
||||||
|
AuthProvider,
|
||||||
])
|
])
|
||||||
.entity(AuthSession, [AuthStore])
|
.entity(AuthSession, [AuthStore])
|
||||||
.service(SubscriptionService, [SubscriptionStore])
|
.service(SubscriptionService, [SubscriptionStore])
|
||||||
|
|||||||
22
packages/frontend/core/src/modules/cloud/provider/auth.ts
Normal file
22
packages/frontend/core/src/modules/cloud/provider/auth.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { createIdentifier } from '@toeverything/infra';
|
||||||
|
|
||||||
|
export interface AuthProvider {
|
||||||
|
signInMagicLink(email: string, token: string): Promise<void>;
|
||||||
|
|
||||||
|
signInOauth(
|
||||||
|
code: string,
|
||||||
|
state: string,
|
||||||
|
provider: string
|
||||||
|
): Promise<{ redirectUri?: string }>;
|
||||||
|
|
||||||
|
signInPassword(credential: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
verifyToken?: string;
|
||||||
|
challenge?: string;
|
||||||
|
}): Promise<void>;
|
||||||
|
|
||||||
|
signOut(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthProvider = createIdentifier<AuthProvider>('AuthProvider');
|
||||||
@@ -112,13 +112,7 @@ export class AuthService extends Service {
|
|||||||
async signInMagicLink(email: string, token: string, byLink = true) {
|
async signInMagicLink(email: string, token: string, byLink = true) {
|
||||||
const method = byLink ? 'magic-link' : 'otp';
|
const method = byLink ? 'magic-link' : 'otp';
|
||||||
try {
|
try {
|
||||||
await this.fetchService.fetch('/api/auth/magic-link', {
|
await this.store.signInMagicLink(email, token);
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email, token }),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.session.revalidate();
|
this.session.revalidate();
|
||||||
track.$.$.auth.signedIn({ method });
|
track.$.$.auth.signedIn({ method });
|
||||||
@@ -160,7 +154,7 @@ export class AuthService extends Service {
|
|||||||
);
|
);
|
||||||
url = oauthUrl.toString();
|
url = oauthUrl.toString();
|
||||||
|
|
||||||
return url;
|
return url as string;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
track.$.$.auth.signInFail({
|
track.$.$.auth.signInFail({
|
||||||
method: 'oauth',
|
method: 'oauth',
|
||||||
@@ -173,18 +167,16 @@ export class AuthService extends Service {
|
|||||||
|
|
||||||
async signInOauth(code: string, state: string, provider: string) {
|
async signInOauth(code: string, state: string, provider: string) {
|
||||||
try {
|
try {
|
||||||
const res = await this.fetchService.fetch('/api/oauth/callback', {
|
const { redirectUri } = await this.store.signInOauth(
|
||||||
method: 'POST',
|
code,
|
||||||
body: JSON.stringify({ code, state }),
|
state,
|
||||||
headers: {
|
provider
|
||||||
'content-type': 'application/json',
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.session.revalidate();
|
this.session.revalidate();
|
||||||
|
|
||||||
track.$.$.auth.signedIn({ method: 'oauth', provider });
|
track.$.$.auth.signedIn({ method: 'oauth', provider });
|
||||||
return await res.json();
|
return { redirectUri };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
track.$.$.auth.signInFail({
|
track.$.$.auth.signInFail({
|
||||||
method: 'oauth',
|
method: 'oauth',
|
||||||
@@ -203,16 +195,7 @@ export class AuthService extends Service {
|
|||||||
}) {
|
}) {
|
||||||
track.$.$.auth.signIn({ method: 'password' });
|
track.$.$.auth.signIn({ method: 'password' });
|
||||||
try {
|
try {
|
||||||
await this.fetchService.fetch('/api/auth/sign-in', {
|
await this.store.signInPassword(credential);
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(credential),
|
|
||||||
headers: {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
...(credential.verifyToken
|
|
||||||
? this.captchaHeaders(credential.verifyToken, credential.challenge)
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.session.revalidate();
|
this.session.revalidate();
|
||||||
track.$.$.auth.signedIn({ method: 'password' });
|
track.$.$.auth.signedIn({ method: 'password' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -225,7 +208,7 @@ export class AuthService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async signOut() {
|
async signOut() {
|
||||||
await this.fetchService.fetch('/api/auth/sign-out');
|
await this.store.signOut();
|
||||||
this.store.setCachedAuthSession(null);
|
this.store.setCachedAuthSession(null);
|
||||||
this.session.revalidate();
|
this.session.revalidate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Store } from '@toeverything/infra';
|
|||||||
|
|
||||||
import type { GlobalState } from '../../storage';
|
import type { GlobalState } from '../../storage';
|
||||||
import type { AuthSessionInfo } from '../entities/session';
|
import type { AuthSessionInfo } from '../entities/session';
|
||||||
|
import type { AuthProvider } from '../provider/auth';
|
||||||
import type { FetchService } from '../services/fetch';
|
import type { FetchService } from '../services/fetch';
|
||||||
import type { GraphQLService } from '../services/graphql';
|
import type { GraphQLService } from '../services/graphql';
|
||||||
import type { ServerService } from '../services/server';
|
import type { ServerService } from '../services/server';
|
||||||
@@ -25,7 +26,8 @@ export class AuthStore extends Store {
|
|||||||
private readonly fetchService: FetchService,
|
private readonly fetchService: FetchService,
|
||||||
private readonly gqlService: GraphQLService,
|
private readonly gqlService: GraphQLService,
|
||||||
private readonly globalState: GlobalState,
|
private readonly globalState: GlobalState,
|
||||||
private readonly serverService: ServerService
|
private readonly serverService: ServerService,
|
||||||
|
private readonly authProvider: AuthProvider
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -63,6 +65,27 @@ export class AuthStore extends Store {
|
|||||||
return data; // Return null if data empty
|
return data; // Return null if data empty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async signInMagicLink(email: string, token: string) {
|
||||||
|
await this.authProvider.signInMagicLink(email, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async signInOauth(code: string, state: string, provider: string) {
|
||||||
|
return await this.authProvider.signInOauth(code, state, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
async signInPassword(credential: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
verifyToken?: string;
|
||||||
|
challenge?: string;
|
||||||
|
}) {
|
||||||
|
await this.authProvider.signInPassword(credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
async signOut() {
|
||||||
|
await this.authProvider.signOut();
|
||||||
|
}
|
||||||
|
|
||||||
async uploadAvatar(file: File) {
|
async uploadAvatar(file: File) {
|
||||||
await this.gqlService.gql({
|
await this.gqlService.gql({
|
||||||
query: uploadAvatarMutation,
|
query: uploadAvatarMutation,
|
||||||
|
|||||||
@@ -628,6 +628,7 @@ __metadata:
|
|||||||
"@types/react-dom": "npm:^19.0.2"
|
"@types/react-dom": "npm:^19.0.2"
|
||||||
async-call-rpc: "npm:^6.4.2"
|
async-call-rpc: "npm:^6.4.2"
|
||||||
cross-env: "npm:^7.0.3"
|
cross-env: "npm:^7.0.3"
|
||||||
|
idb: "npm:^8.0.0"
|
||||||
next-themes: "npm:^0.4.4"
|
next-themes: "npm:^0.4.4"
|
||||||
react: "npm:^19.0.0"
|
react: "npm:^19.0.0"
|
||||||
react-dom: "npm:^19.0.0"
|
react-dom: "npm:^19.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user