Add provider-neutral Google authentication

This commit is contained in:
hz
2026-07-20 21:42:56 -05:00
parent 48e19f138e
commit 72fd1e3e0f
20 changed files with 1084 additions and 36 deletions
+119
View File
@@ -6,6 +6,8 @@ signal preferences_saved
signal password_changed
signal account_deleted
signal archive_saved(path: String)
signal external_link_started(provider: String)
signal external_linked(provider: String)
const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd")
const AUTH_URL := "https://us3q.ranaze.com/api/Auth"
@@ -15,11 +17,18 @@ var account: Dictionary = {}
var preferences: Dictionary = _default_preferences()
var auth_request: HTTPRequest
var profile_request: HTTPRequest
var external_request: HTTPRequest
var external_poll_timer: Timer
var auth_operation := ""
var profile_operation := ""
var auth_token := ""
var profile_token := ""
var preferences_pending := false
var external_operation := ""
var external_provider := ""
var external_provider_name := ""
var external_transaction_id := ""
var external_poll_deadline_msec := 0
func _ready() -> void:
@@ -31,6 +40,14 @@ func _ready() -> void:
profile_request.timeout = 30.0
profile_request.request_completed.connect(_on_profile_completed)
add_child(profile_request)
external_request = HTTPRequest.new()
external_request.timeout = 25.0
external_request.request_completed.connect(_on_external_completed)
add_child(external_request)
external_poll_timer = Timer.new()
external_poll_timer.wait_time = 2.0
external_poll_timer.timeout.connect(_poll_external_link)
add_child(external_poll_timer)
AuthState.auth_changed.connect(_on_auth_changed)
@@ -83,6 +100,32 @@ func delete_account(password: String, confirmation: String) -> void:
})
func connect_provider(provider: String, provider_name: String) -> void:
if not AuthState.can_send_authenticated_request():
request_failed.emit("Sign in again before connecting an account.")
return
if external_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
request_failed.emit("Another connection request is still in progress.")
return
external_provider = provider
external_provider_name = provider_name
var storage_key := _external_storage_key()
if OS.has_feature("web"):
var popup_result: Variant = JavaScriptBridge.eval(
"window.localStorage.removeItem(%s); var bridge = new URL('/external-redirect.html?key=' + encodeURIComponent(%s) + '&provider=' + encodeURIComponent(%s), window.location.origin).href; window.ranazeProfileExternalWindow = window.open(bridge, 'ranaze_profile_external', 'popup=yes,width=560,height=720,resizable=yes,scrollbars=yes'); window.ranazeProfileExternalWindow !== null;" % [JSON.stringify(storage_key), JSON.stringify(storage_key), JSON.stringify(provider_name)], true)
if not bool(popup_result):
request_failed.emit("Allow popups for this site, then try again.")
_reset_external_link()
return
external_operation = "start"
var headers := PackedStringArray(["Authorization: Bearer %s" % AuthState.access_token, "Content-Type: application/json"])
var error := external_request.request("%s/external/%s/start" % [_auth_base(), provider], headers,
HTTPClient.METHOD_POST, JSON.stringify({"purpose": "link"}))
if error != OK:
request_failed.emit("The account provider could not be reached.")
_reset_external_link()
func _request_auth(operation: String, method: HTTPClient.Method, path: String, payload: Variant = null) -> void:
if auth_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
request_failed.emit("Another account request is still in progress.")
@@ -162,6 +205,82 @@ func _on_profile_completed(result: int, response_code: int, _headers: PackedStri
_save_archive(JSON.stringify(archive, " "))
func _on_external_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
var operation := external_operation
external_operation = ""
if response_code == 401:
AuthState.handle_unauthorized(AuthState.access_token)
_reset_external_link()
return
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
request_failed.emit(_error_message(body, "The external account request failed."))
_reset_external_link()
return
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
if typeof(parsed) != TYPE_DICTIONARY:
request_failed.emit("The external account service returned invalid data.")
_reset_external_link()
return
var data: Dictionary = parsed
if operation == "start":
external_transaction_id = str(data.get("transactionId", ""))
var auth_url := str(data.get("authUrl", ""))
if external_transaction_id.is_empty() or auth_url.is_empty():
request_failed.emit("The provider returned an invalid response.")
_reset_external_link()
return
if OS.has_feature("web"):
JavaScriptBridge.eval("var url=%s; window.localStorage.setItem(%s,url); if(window.ranazeProfileExternalWindow&&!window.ranazeProfileExternalWindow.closed){window.ranazeProfileExternalWindow.location.replace(url);window.ranazeProfileExternalWindow.focus();}" % [JSON.stringify(auth_url), JSON.stringify(_external_storage_key())], true)
else:
OS.shell_open(auth_url)
external_poll_deadline_msec = Time.get_ticks_msec() + 300000
external_poll_timer.start()
external_link_started.emit(external_provider)
elif operation == "status":
match str(data.get("status", "pending")):
"linked":
var linked_provider := external_provider
_reset_external_link()
external_linked.emit(linked_provider)
refresh()
"failed":
request_failed.emit(str(data.get("message", "The provider could not connect this account.")))
_reset_external_link()
func _poll_external_link() -> void:
if external_transaction_id.is_empty() or external_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
return
if Time.get_ticks_msec() >= external_poll_deadline_msec:
request_failed.emit("The account connection timed out.")
_reset_external_link()
return
external_operation = "status"
var error := external_request.request("%s/external/%s/status/%s" % [_auth_base(), external_provider, external_transaction_id])
if error != OK:
external_operation = ""
func _reset_external_link() -> void:
if external_poll_timer != null:
external_poll_timer.stop()
if OS.has_feature("web") and not external_provider.is_empty():
JavaScriptBridge.eval("window.localStorage.removeItem(%s);if(window.ranazeProfileExternalWindow&&!window.ranazeProfileExternalWindow.closed){window.ranazeProfileExternalWindow.close();}window.ranazeProfileExternalWindow=null;" % JSON.stringify(_external_storage_key()), true)
external_operation = ""
external_provider = ""
external_provider_name = ""
external_transaction_id = ""
external_poll_deadline_msec = 0
func _external_storage_key() -> String:
return "ranaze_profile_external_auth_url_%s" % external_provider
func _auth_base() -> String:
return EndpointConfig.resolve(str(ProjectSettings.get_setting("step3/auth_api_base_url", AUTH_URL)), "/api/Auth").rstrip("/")
func _save_archive(content: String) -> void:
var filename := "ranaze-account-export-%s.json" % Time.get_date_string_from_system()
if OS.has_feature("web"):