| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- 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:
- * <spry-component name="LoginFormComponent" sid="login-form"/>
- *
- * // Or create via factory and configure:
- * var login_form = factory.create<LoginFormComponent>();
- * 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<UserService>();
- private SessionService _session_service = inject<SessionService>();
- private PermissionService? _permission_service = inject<PermissionService>();
- private HttpContext _http_context = inject<HttpContext>();
- // =========================================================================
- // 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 """
- <spry-context property="redirect_url"/>
- <script spry-res="htmx.js"></script>
- <div class="spry-login-form" sid="login-form" hx-swap="outerHTML">
- <form sid="form" spry-action=":login" spry-target="login-form" hx-disabled-elt="find button">
- <div class="form-group">
- <label for="username">Username or Email</label>
- <input type="text"
- name="username"
- sid="username-input"
- required
- autocomplete="username"
- autofocus
- placeholder="Enter your username or email"/>
- </div>
- <div class="form-group">
- <label for="password">Password</label>
- <input type="password"
- name="password"
- sid="password-input"
- required
- autocomplete="current-password"
- placeholder="Enter your password"/>
- </div>
- <div class="form-group form-group-checkbox">
- <label class="checkbox-label">
- <input type="checkbox" name="remember_me" sid="remember-me-input"/>
- <span>Remember me</span>
- </label>
- </div>
- <div spry-if="this.error_message != null" class="error-message" sid="error-container">
- <span content-expr="this.error_message"></span>
- </div>
- <button type="submit" sid="submit-btn" class="login-btn">Log In</button>
- </form>
- </div>
- """;
- }}
- 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;
- }
- }
- }
|