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;
}
}
}