Add study selection and account registration flows
This commit is contained in:
+239
-54
@@ -2,13 +2,30 @@ extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
const AUTH_LOGIN_URL := "https://pauth.ranaze.com/api/Auth/login"
|
||||
const AUTH_REGISTER_URL := "https://pauth.ranaze.com/api/Auth/register"
|
||||
|
||||
var main: Control
|
||||
var auth_panel: PanelContainer
|
||||
var login_tab: Button
|
||||
var register_tab: Button
|
||||
var login_fields: VBoxContainer
|
||||
var register_fields: VBoxContainer
|
||||
var username_input: LineEdit
|
||||
var password_input: LineEdit
|
||||
var registration_username_input: LineEdit
|
||||
var registration_email_input: LineEdit
|
||||
var registration_password_input: LineEdit
|
||||
var registration_confirm_input: LineEdit
|
||||
var login_button: Button
|
||||
var register_button: Button
|
||||
var error_label: Label
|
||||
var success_label: Label
|
||||
var login_request: HTTPRequest
|
||||
var register_request: HTTPRequest
|
||||
var auth_mode := "login"
|
||||
var automatic_login_pending := false
|
||||
var login_attempt_username := ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
@@ -22,57 +39,82 @@ func _ready() -> void:
|
||||
center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
add_child(center)
|
||||
|
||||
var panel := PanelContainer.new()
|
||||
panel.custom_minimum_size = Vector2(520, 430)
|
||||
panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Color("2bdbff")))
|
||||
center.add_child(panel)
|
||||
auth_panel = PanelContainer.new()
|
||||
auth_panel.custom_minimum_size = Vector2(540, 470)
|
||||
auth_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Style.CYAN))
|
||||
center.add_child(auth_panel)
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.add_theme_constant_override("separation", 14)
|
||||
panel.add_child(content)
|
||||
auth_panel.add_child(content)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "STEP 3 Arcade"
|
||||
title.text = "RanaZe's Doctor Maker"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
Style.set_label(title, 36, Style.TEXT)
|
||||
Style.set_label(title, 34, Style.TEXT)
|
||||
content.add_child(title)
|
||||
|
||||
var subtitle := Label.new()
|
||||
subtitle.text = "Sign in with your Promiscuity account to start studying."
|
||||
subtitle.text = "Sign in or create an account to start studying."
|
||||
subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(subtitle, 16, Style.MUTED)
|
||||
content.add_child(subtitle)
|
||||
|
||||
var username_label := Label.new()
|
||||
username_label.text = "Username"
|
||||
Style.set_label(username_label, 15, Style.CYAN)
|
||||
content.add_child(username_label)
|
||||
var tabs := HBoxContainer.new()
|
||||
tabs.add_theme_constant_override("separation", 8)
|
||||
content.add_child(tabs)
|
||||
|
||||
username_input = LineEdit.new()
|
||||
username_input.placeholder_text = "enter username"
|
||||
username_input.custom_minimum_size = Vector2(0, 44)
|
||||
username_input.text_submitted.connect(_on_input_submitted)
|
||||
content.add_child(username_input)
|
||||
login_tab = Button.new()
|
||||
login_tab.text = "Log In"
|
||||
login_tab.custom_minimum_size = Vector2(0, 44)
|
||||
login_tab.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
login_tab.pressed.connect(func(): _set_auth_mode("login"))
|
||||
tabs.add_child(login_tab)
|
||||
|
||||
var password_label := Label.new()
|
||||
password_label.text = "Password"
|
||||
Style.set_label(password_label, 15, Style.CYAN)
|
||||
content.add_child(password_label)
|
||||
register_tab = Button.new()
|
||||
register_tab.text = "Create Account"
|
||||
register_tab.custom_minimum_size = Vector2(0, 44)
|
||||
register_tab.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
register_tab.pressed.connect(func(): _set_auth_mode("register"))
|
||||
tabs.add_child(register_tab)
|
||||
|
||||
password_input = LineEdit.new()
|
||||
password_input.placeholder_text = "enter password"
|
||||
password_input.secret = true
|
||||
password_input.custom_minimum_size = Vector2(0, 44)
|
||||
password_input.text_submitted.connect(_on_input_submitted)
|
||||
content.add_child(password_input)
|
||||
login_fields = VBoxContainer.new()
|
||||
login_fields.add_theme_constant_override("separation", 10)
|
||||
content.add_child(login_fields)
|
||||
username_input = _add_text_field(login_fields, "Username", "enter username")
|
||||
username_input.text_submitted.connect(_on_login_input_submitted)
|
||||
password_input = _add_text_field(login_fields, "Password", "enter password", true)
|
||||
password_input.text_submitted.connect(_on_login_input_submitted)
|
||||
|
||||
login_button = Button.new()
|
||||
login_button.text = "Log In"
|
||||
login_button.custom_minimum_size = Vector2(0, 50)
|
||||
Style.style_primary_button(login_button)
|
||||
login_button.pressed.connect(_on_login_pressed)
|
||||
content.add_child(login_button)
|
||||
login_fields.add_child(login_button)
|
||||
|
||||
register_fields = VBoxContainer.new()
|
||||
register_fields.add_theme_constant_override("separation", 10)
|
||||
content.add_child(register_fields)
|
||||
registration_username_input = _add_text_field(register_fields, "Username", "3-24 letters, numbers, or underscores")
|
||||
registration_email_input = _add_text_field(register_fields, "Email", "[email protected]")
|
||||
registration_password_input = _add_text_field(register_fields, "Password", "at least 8 characters", true)
|
||||
registration_confirm_input = _add_text_field(register_fields, "Confirm password", "re-enter password", true)
|
||||
registration_confirm_input.text_submitted.connect(_on_register_input_submitted)
|
||||
|
||||
register_button = Button.new()
|
||||
register_button.text = "Create Account"
|
||||
register_button.custom_minimum_size = Vector2(0, 50)
|
||||
Style.style_primary_button(register_button)
|
||||
register_button.pressed.connect(_on_register_pressed)
|
||||
register_fields.add_child(register_button)
|
||||
|
||||
success_label = Label.new()
|
||||
success_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
success_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(success_label, 15, Style.LIME)
|
||||
content.add_child(success_label)
|
||||
|
||||
error_label = Label.new()
|
||||
error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
@@ -89,47 +131,90 @@ func _ready() -> void:
|
||||
login_request.request_completed.connect(_on_login_request_completed)
|
||||
add_child(login_request)
|
||||
|
||||
register_request = HTTPRequest.new()
|
||||
register_request.name = "RegisterRequest"
|
||||
register_request.request_completed.connect(_on_register_request_completed)
|
||||
add_child(register_request)
|
||||
_set_auth_mode("login")
|
||||
|
||||
|
||||
func on_show(_payload: Variant = null) -> void:
|
||||
error_label.text = ""
|
||||
login_button.disabled = false
|
||||
login_button.text = "Log In"
|
||||
_set_busy(false)
|
||||
_set_auth_mode("login")
|
||||
_clear_messages()
|
||||
username_input.grab_focus()
|
||||
|
||||
|
||||
func _add_text_field(parent: VBoxContainer, field_name: String, placeholder: String, secret: bool = false) -> LineEdit:
|
||||
var label := Label.new()
|
||||
label.text = field_name
|
||||
Style.set_label(label, 15, Style.CYAN)
|
||||
parent.add_child(label)
|
||||
|
||||
var input := LineEdit.new()
|
||||
input.placeholder_text = placeholder
|
||||
input.secret = secret
|
||||
input.custom_minimum_size = Vector2(0, 42)
|
||||
parent.add_child(input)
|
||||
return input
|
||||
|
||||
|
||||
func _set_auth_mode(next_mode: String) -> void:
|
||||
auth_mode = next_mode
|
||||
if login_fields == null:
|
||||
return
|
||||
login_fields.visible = auth_mode == "login"
|
||||
register_fields.visible = auth_mode == "register"
|
||||
auth_panel.custom_minimum_size.y = 470 if auth_mode == "login" else 650
|
||||
if auth_mode == "login":
|
||||
Style.style_primary_button(login_tab)
|
||||
Style.style_chip(register_tab, Style.CYAN)
|
||||
username_input.grab_focus()
|
||||
else:
|
||||
Style.style_chip(login_tab, Style.CYAN)
|
||||
Style.style_primary_button(register_tab)
|
||||
registration_username_input.grab_focus()
|
||||
_clear_messages()
|
||||
|
||||
|
||||
func _on_login_pressed() -> void:
|
||||
var username := username_input.text.strip_edges()
|
||||
var password := password_input.text
|
||||
_send_login(username_input.text.strip_edges(), password_input.text, false)
|
||||
|
||||
|
||||
func _send_login(username: String, password: String, is_automatic: bool) -> void:
|
||||
if username.is_empty() or password.is_empty():
|
||||
_show_error("Username and password required.")
|
||||
return
|
||||
|
||||
login_button.disabled = true
|
||||
automatic_login_pending = is_automatic
|
||||
login_attempt_username = username
|
||||
_set_busy(true)
|
||||
login_button.text = "Signing in..."
|
||||
error_label.text = ""
|
||||
|
||||
var payload := {
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
_clear_messages()
|
||||
if is_automatic:
|
||||
success_label.text = "Account created. Signing you in..."
|
||||
var payload := {"username": username, "password": password}
|
||||
var headers := PackedStringArray(["Content-Type: application/json"])
|
||||
var err := login_request.request(AUTH_LOGIN_URL, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
|
||||
if err != OK:
|
||||
login_button.disabled = false
|
||||
var request_error := login_request.request(AUTH_LOGIN_URL, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
|
||||
if request_error != OK:
|
||||
_set_busy(false)
|
||||
login_button.text = "Log In"
|
||||
_show_error("Failed to send login request.")
|
||||
|
||||
|
||||
func _on_login_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
login_button.disabled = false
|
||||
_set_busy(false)
|
||||
login_button.text = "Log In"
|
||||
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
_show_error("Network error. Please try again.")
|
||||
return
|
||||
|
||||
if response_code < 200 or response_code >= 300:
|
||||
_show_error("Login failed. Check your credentials.")
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||
if automatic_login_pending:
|
||||
_set_auth_mode("login")
|
||||
username_input.text = login_attempt_username
|
||||
password_input.text = ""
|
||||
_show_error("Account created, but automatic login failed. Please log in with your new account.")
|
||||
else:
|
||||
_show_error("Login failed. Check your credentials and try again.")
|
||||
automatic_login_pending = false
|
||||
return
|
||||
|
||||
var response: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||||
@@ -138,13 +223,13 @@ func _on_login_request_completed(result: int, response_code: int, _headers: Pack
|
||||
return
|
||||
|
||||
var token := String(response.get("accessToken", ""))
|
||||
var username := String(response.get("username", username_input.text.strip_edges()))
|
||||
var response_username := String(response.get("username", login_attempt_username))
|
||||
if token.is_empty():
|
||||
_show_error("Login response did not include an access token.")
|
||||
return
|
||||
|
||||
AuthState.set_session(
|
||||
username,
|
||||
response_username,
|
||||
token,
|
||||
String(response.get("refreshToken", "")),
|
||||
String(response.get("role", "")),
|
||||
@@ -152,12 +237,112 @@ func _on_login_request_completed(result: int, response_code: int, _headers: Pack
|
||||
)
|
||||
QuestionBankService.load_questions_from_api()
|
||||
password_input.text = ""
|
||||
main.show_screen("dashboard")
|
||||
registration_password_input.text = ""
|
||||
registration_confirm_input.text = ""
|
||||
automatic_login_pending = false
|
||||
main.show_screen("selection", "exam")
|
||||
|
||||
|
||||
func _on_input_submitted(_new_text: String) -> void:
|
||||
func _on_register_pressed() -> void:
|
||||
var username := registration_username_input.text.strip_edges()
|
||||
var email := registration_email_input.text.strip_edges()
|
||||
var password := registration_password_input.text
|
||||
var confirmation := registration_confirm_input.text
|
||||
var validation_error := _validate_registration(username, email, password, confirmation)
|
||||
if validation_error != "":
|
||||
_show_error(validation_error)
|
||||
return
|
||||
|
||||
_set_busy(true)
|
||||
register_button.text = "Creating account..."
|
||||
_clear_messages()
|
||||
var payload := {"username": username, "email": email, "password": password}
|
||||
var headers := PackedStringArray(["Content-Type: application/json"])
|
||||
var request_error := register_request.request(AUTH_REGISTER_URL, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
|
||||
if request_error != OK:
|
||||
_set_busy(false)
|
||||
register_button.text = "Create Account"
|
||||
_show_error("Failed to send account creation request.")
|
||||
|
||||
|
||||
func _on_register_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
_set_busy(false)
|
||||
register_button.text = "Create Account"
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
_clear_registration_passwords()
|
||||
_show_error("Network error. Please try again.")
|
||||
return
|
||||
if response_code < 200 or response_code >= 300:
|
||||
_clear_registration_passwords()
|
||||
var server_message := _response_message(body)
|
||||
_show_error(server_message if server_message != "" else "Account creation failed.")
|
||||
return
|
||||
|
||||
var username := registration_username_input.text.strip_edges()
|
||||
var password := registration_password_input.text
|
||||
username_input.text = username
|
||||
_send_login(username, password, true)
|
||||
|
||||
|
||||
func _validate_registration(username: String, email: String, password: String, confirmation: String) -> String:
|
||||
var username_pattern := RegEx.new()
|
||||
username_pattern.compile("^[A-Za-z0-9_]{3,24}$")
|
||||
if username_pattern.search(username) == null:
|
||||
registration_username_input.grab_focus()
|
||||
return "Username must be 3-24 characters using only letters, numbers, or underscores."
|
||||
var email_pattern := RegEx.new()
|
||||
email_pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")
|
||||
if email_pattern.search(email) == null:
|
||||
registration_email_input.grab_focus()
|
||||
return "Enter a valid email address."
|
||||
if password.length() < 8:
|
||||
registration_password_input.grab_focus()
|
||||
return "Password must be at least 8 characters."
|
||||
if password != confirmation:
|
||||
registration_confirm_input.grab_focus()
|
||||
return "Passwords do not match."
|
||||
return ""
|
||||
|
||||
|
||||
func _set_busy(is_busy: bool) -> void:
|
||||
if login_button == null:
|
||||
return
|
||||
login_button.disabled = is_busy
|
||||
register_button.disabled = is_busy
|
||||
login_tab.disabled = is_busy
|
||||
register_tab.disabled = is_busy
|
||||
username_input.editable = not is_busy
|
||||
password_input.editable = not is_busy
|
||||
registration_username_input.editable = not is_busy
|
||||
registration_email_input.editable = not is_busy
|
||||
registration_password_input.editable = not is_busy
|
||||
registration_confirm_input.editable = not is_busy
|
||||
|
||||
|
||||
func _clear_registration_passwords() -> void:
|
||||
registration_password_input.text = ""
|
||||
registration_confirm_input.text = ""
|
||||
|
||||
|
||||
func _response_message(body: PackedByteArray) -> String:
|
||||
var raw := body.get_string_from_utf8().strip_edges()
|
||||
var parsed: Variant = JSON.parse_string(raw)
|
||||
return String(parsed) if typeof(parsed) == TYPE_STRING else raw
|
||||
|
||||
|
||||
func _on_login_input_submitted(_new_text: String) -> void:
|
||||
_on_login_pressed()
|
||||
|
||||
|
||||
func _on_register_input_submitted(_new_text: String) -> void:
|
||||
_on_register_pressed()
|
||||
|
||||
|
||||
func _clear_messages() -> void:
|
||||
error_label.text = ""
|
||||
success_label.text = ""
|
||||
|
||||
|
||||
func _show_error(message: String) -> void:
|
||||
success_label.text = ""
|
||||
error_label.text = message
|
||||
|
||||
Reference in New Issue
Block a user