using Spry; using Inversion; using Astralis; using Invercargill.DataStructures; namespace Spry.Authentication.Components { /** * LoginFormComponent - A reusable login form component with HTMX-based submission. * * This component provides a complete login flow: * - Displays login form with username/email and password fields * - Handles form submission via HTMX * - Authenticates users via UserService * - Creates sessions and sets cookies via SessionService * - Displays error messages on failed authentication * - Redirects to configurable URL on success * * Usage: * // In a PageComponent or another component's markup: * * * // Or create via factory and configure: * var login_form = factory.create(); * login_form.redirect_url = "/dashboard"; * * Customization: * - redirect_url: URL to redirect after successful login (default: "/") * - login_action_name: Custom action name (default: "login") * - Override markup property for custom styling * * This component uses the inject<> pattern for dependency injection. */ public class LoginFormComponent : Component { private UserService _user_service = inject(); private SessionService _session_service = inject(); private PermissionService? _permission_service = inject(); private HttpContext _http_context = inject(); // ========================================================================= // Configuration Properties // ========================================================================= /** * URL to redirect to after successful login. * Default: "/" */ public string redirect_url { get; set; default = "/"; } /** * Duration for "remember me" sessions in hours. * When "remember_me" is checked, session duration is extended. * Default: 168 (7 days) */ public int remember_me_duration_hours { get; set; default = 168; } // ========================================================================= // State Properties // ========================================================================= /** * Error message to display (set after failed authentication). */ public string? error_message { get; private set; default = null; } /** * Username value to preserve after failed login. */ public string preserved_username { get; private set; default = ""; } /** * Whether login was successful (triggers redirect). */ public bool login_successful { get; private set; default = false; } // ========================================================================= // Component Implementation // ========================================================================= public override string markup { get { return """ """; }} public override async void prepare() throws Error { // Preserve username in the input field after failed login if (preserved_username.length > 0) { this["username-input"].set_attribute("value", preserved_username); } } public async override void handle_action(string action) throws Error { // Normalize action name comparison if (action == "login") { yield handle_login_async(); } } // ========================================================================= // Login Handler // ========================================================================= private async void handle_login_async() throws Error { stdout.printf("LOGIN DEBUG: handle_login_async() triggered\n"); var query = _http_context.request.query_params; // Get form values var username_raw = query.get_any_or_default("username"); var password_raw = query.get_any_or_default("password"); var remember_me_raw = query.get_any_or_default("remember_me"); var username = username_raw != null ? ((!)username_raw).strip() : ""; var password = password_raw ?? ""; var remember_me = remember_me_raw == "on"; stdout.printf("LOGIN DEBUG: Username/email received: '%s'\n", username); stdout.printf("LOGIN DEBUG: Password length: %d\n", password.length); stdout.printf("LOGIN DEBUG: Remember me: %s\n", remember_me ? "true" : "false"); // Preserve username for re-display on error preserved_username = username; // Validate inputs if (username.length == 0) { stdout.printf("LOGIN DEBUG: Validation failed - empty username\n"); error_message = "Please enter your username or email"; return; } if (password.length == 0) { stdout.printf("LOGIN DEBUG: Validation failed - empty password\n"); error_message = "Please enter your password"; return; } // Attempt authentication stdout.printf("LOGIN DEBUG: Calling authenticate_async()...\n"); var user = yield _user_service.authenticate_async(username, password); stdout.printf("LOGIN DEBUG: authenticate_async() returned user: %s\n", user != null ? user.id : "null"); if (user == null) { // Authentication failed - show generic error message // (Don't reveal whether username or password was wrong for security) stdout.printf("LOGIN DEBUG: Authentication failed - invalid credentials\n"); error_message = "Invalid username or password"; return; } stdout.printf("LOGIN DEBUG: Authentication successful for user: %s (username: %s, email: %s)\n", user.id, user.username, user.email); // Get client info for session tracking var ip_address = _http_context.request.remote_address; var user_agent = _http_context.request.headers.get_any_or_default("User-Agent"); stdout.printf("LOGIN DEBUG: Client IP: %s, User-Agent: %s\n", ip_address ?? "null", user_agent ?? "null"); // Create session stdout.printf("LOGIN DEBUG: Creating session...\n"); Session? session; if (remember_me) { // Create session with extended duration for "remember me" // Note: Current SessionService uses configured duration; // for extended sessions, we'd need to modify SessionService // For now, create a regular session session = yield _session_service.create_session_async(user.id, ip_address, user_agent); } else { session = yield _session_service.create_session_async(user.id, ip_address, user_agent); } stdout.printf("LOGIN DEBUG: Session creation result: %s\n", session != null ? session.id : "null"); if (session == null) { stdout.printf("LOGIN DEBUG: ERROR - Failed to create session\n"); error_message = "Failed to create session. Please try again."; return; } // Generate session token stdout.printf("LOGIN DEBUG: Generating session token...\n"); var token = _session_service.generate_session_token(session); stdout.printf("LOGIN DEBUG: Token generated (length: %d)\n", token.length); // Set session cookie using ResponseState // This accumulates the cookie header to be applied when to_result() is called stdout.printf("LOGIN DEBUG: Setting session cookie via ResponseState...\n"); response.set_cookie("spry_session", token, 86400, "/", true, true, "Strict"); stdout.printf("LOGIN DEBUG: Session cookie set\n"); // Set up HTMX redirect using ResponseState // This sets the HX-Redirect header for client-side redirect response.redirect(redirect_url); login_successful = true; error_message = null; stdout.printf("LOGIN DEBUG: Login successful! Redirect to: %s\n", redirect_url); // Optional: Check permissions if permission_service is available // This can be used for post-login permission checks if (_permission_service != null) { // Subclasses can override to add permission checks // e.g., require certain permissions to access specific areas } } // ========================================================================= // Public API // ========================================================================= /** * Clears the error message and resets the form state. */ public void clear_error() { error_message = null; } /** * Sets a custom error message. * Useful for external validation or custom error handling. * * @param message The error message to display */ public void set_error(string message) { error_message = message; } } }