Jelajahi Sumber

Fix authentication default state to be disabled (fixes #27)

- Change default authentication method from JWT to NONE in server_config.h
- Update UserManager initialization in main.cpp to use the configured auth method
- Update AuthMiddleware factory default to use NONE and enable guest access
- Authentication is now disabled by default, users must explicitly enable it with --auth-method parameter
Fszontagh 3 bulan lalu
induk
melakukan
f6b678331d

+ 93 - 0
.roo/mcp.json

@@ -0,0 +1,93 @@
+{
+    "mcpServers": {
+        "gogs-mcp": {
+            "command": "node",
+            "args": [
+                "/data/gogs-mcp/dist/index.js"
+            ],
+            "env": {
+                "GOGS_ACCESS_TOKEN": "5c332ecdfea7813602bbc52930334c3853732791",
+                "GOGS_SERVER_URL": "https://git.fsociety.hu"
+            },
+            "alwaysAllow": [
+                "get_current_user",
+                "get_user",
+                "search_users",
+                "list_user_repositories",
+                "search_repositories",
+                "get_repository",
+                "create_repository",
+                "delete_repository",
+                "get_contents",
+                "get_raw_content",
+                "list_branches",
+                "get_tree",
+                "list_issues",
+                "get_commits",
+                "get_issue",
+                "create_issue",
+                "update_issue",
+                "list_issue_comments",
+                "create_issue_comment",
+                "update_issue_comment",
+                "list_labels",
+                "get_label",
+                "create_label",
+                "update_label",
+                "delete_label",
+                "list_issue_labels",
+                "add_issue_labels",
+                "remove_issue_label",
+                "replace_issue_labels",
+                "remove_all_issue_labels",
+                "list_milestones",
+                "get_milestone",
+                "create_milestone",
+                "update_milestone",
+                "delete_milestone",
+                "create_organization",
+                "list_public_organizations",
+                "get_organization",
+                "list_user_organizations",
+                "update_organization",
+                "add_organization_member",
+                "list_organization_teams",
+                "create_team",
+                "get_team",
+                "add_team_member",
+                "remove_team_member",
+                "list_releases",
+                "list_collaborators",
+                "check_collaborator",
+                "add_collaborator",
+                "remove_collaborator",
+                "list_user_emails",
+                "add_user_emails",
+                "delete_user_emails",
+                "list_user_keys",
+                "list_my_keys",
+                "get_public_key",
+                "create_public_key",
+                "delete_public_key",
+                "list_hooks",
+                "create_hook",
+                "update_hook",
+                "delete_hook",
+                "list_followers",
+                "list_following",
+                "check_following",
+                "unfollow_user",
+                "follow_user",
+                "list_deploy_keys",
+                "get_deploy_key",
+                "create_deploy_key",
+                "delete_deploy_key",
+                "render_markdown",
+                "render_markdown_raw",
+                "admin_create_user",
+                "admin_edit_user",
+                "admin_delete_user"
+            ]
+        }
+    }
+}

+ 11 - 0
.roo/rules/01-general.md

@@ -0,0 +1,11 @@
+- always commit and push the changes, reference to the issue in the commit message (if issue exists)
+- if issue does not exists, create new one if no similar found
+- always use the abailable mcp tools
+- never use "using" in c++ (for example: using json = nlohmann::json;). If you find one, refactor it
+- in c++, always follor The Rule of 5
+- if changes made, always check if the built-in ui need changes too
+- when user reference to issues, the user thinks to gogs issue, gogs repo
+- the stable diffusion models are in the /data/SD_MODELS folder, always use this when starting the server
+- if possible, keep the build/_deps folder. The rebuild take a very long time
+- keep clean the project directory. For example the output directory, queue directory not required in the project's root
+- when printing out directory / filenames to the console (std::cout for example), always use absolute path

+ 1 - 1
CMakeLists.txt

@@ -41,7 +41,7 @@ include(ExternalProject)
 # Set up external project for stable-diffusion.cpp
 ExternalProject_Add(stable-diffusion.cpp
     GIT_REPOSITORY https://github.com/leejet/stable-diffusion.cpp.git
-    GIT_TAG master-334-d05e46c
+    GIT_TAG master-347-6103d86
     SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/stable-diffusion.cpp-src"
     BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/stable-diffusion.cpp-build"
     CMAKE_ARGS

+ 388 - 0
include/auth_middleware.h

@@ -0,0 +1,388 @@
+#ifndef AUTH_MIDDLEWARE_H
+#define AUTH_MIDDLEWARE_H
+
+#include <string>
+#include <vector>
+#include <functional>
+#include <memory>
+#include "jwt_auth.h"
+#include "user_manager.h"
+#include "server_config.h"
+
+namespace httplib {
+    class Request;
+    class Response;
+}
+
+/**
+ * @brief Authentication context structure
+ */
+struct AuthContext {
+    bool authenticated;                    ///< Authentication status
+    std::string userId;                     ///< User ID
+    std::string username;                   ///< Username
+    std::string role;                       ///< User role
+    std::vector<std::string> permissions;  ///< User permissions
+    std::string authMethod;                 ///< Authentication method used
+    std::string errorMessage;              ///< Error message if authentication failed
+    std::string errorCode;                 ///< Error code for API responses
+};
+
+
+/**
+ * @brief Authentication middleware class
+ *
+ * This class provides authentication and authorization middleware for HTTP requests.
+ * It supports multiple authentication methods (JWT, API keys, Unix auth) and
+ * role-based access control.
+ */
+class AuthMiddleware {
+public:
+    /**
+     * @brief Authentication handler function type
+     *
+     * @param req HTTP request
+     * @param res HTTP response
+     * @param context Authentication context
+     */
+    using AuthHandler = std::function<void(const httplib::Request& req,
+                                           httplib::Response& res,
+                                           const AuthContext& context)>;
+
+    /**
+     * @brief Construct a new Auth Middleware object
+     *
+     * @param config Authentication configuration
+     * @param userManager User manager instance
+     */
+    explicit AuthMiddleware(const AuthConfig& config,
+                           std::shared_ptr<UserManager> userManager);
+
+    /**
+     * @brief Destroy the Auth Middleware object
+     */
+    ~AuthMiddleware();
+
+    /**
+     * @brief Initialize the authentication middleware
+     *
+     * @return true if initialization successful, false otherwise
+     */
+    bool initialize();
+
+    /**
+     * @brief Authenticate HTTP request
+     *
+     * @param req HTTP request
+     * @param res HTTP response
+     * @return AuthContext Authentication context
+     */
+    AuthContext authenticate(const httplib::Request& req, httplib::Response& res);
+
+    /**
+     * @brief Check if path requires authentication
+     *
+     * @param path Request path
+     * @return true if authentication required, false otherwise
+     */
+    bool requiresAuthentication(const std::string& path) const;
+
+    /**
+     * @brief Check if path requires admin access
+     *
+     * @param path Request path
+     * @return true if admin access required, false otherwise
+     */
+    bool requiresAdminAccess(const std::string& path) const;
+
+    /**
+     * @brief Check if path requires user access (any authenticated user)
+     *
+     * @param path Request path
+     * @return true if user access required, false otherwise
+     */
+    bool requiresUserAccess(const std::string& path) const;
+
+    /**
+     * @brief Check if user has permission for path
+     *
+     * @param path Request path
+     * @param permissions User permissions
+     * @return true if user has access, false otherwise
+     */
+    bool hasPathAccess(const std::string& path,
+                      const std::vector<std::string>& permissions) const;
+
+    /**
+     * @brief Create authentication middleware handler
+     *
+     * @param handler Next handler in chain
+     * @return AuthHandler Middleware handler function
+     */
+    AuthHandler createMiddleware(AuthHandler handler);
+
+    /**
+     * @brief Send authentication error response
+     *
+     * @param res HTTP response
+     * @param message Error message
+     * @param errorCode Error code
+     * @param statusCode HTTP status code
+     */
+    void sendAuthError(httplib::Response& res,
+                      const std::string& message,
+                      const std::string& errorCode = "AUTH_ERROR",
+                      int statusCode = 401);
+
+    /**
+     * @brief Send authorization error response
+     *
+     * @param res HTTP response
+     * @param message Error message
+     * @param errorCode Error code
+     */
+    void sendAuthzError(httplib::Response& res,
+                       const std::string& message,
+                       const std::string& errorCode = "ACCESS_DENIED");
+
+    /**
+     * @brief Add public path (no authentication required)
+     *
+     * @param path Path to add
+     */
+    void addPublicPath(const std::string& path);
+
+    /**
+     * @brief Add admin-only path
+     *
+     * @param path Path to add
+     */
+    void addAdminPath(const std::string& path);
+
+    /**
+     * @brief Add user-only path
+     *
+     * @param path Path to add
+     */
+    void addUserPath(const std::string& path);
+
+    /**
+     * @brief Set JWT secret
+     *
+     * @param secret JWT secret key
+     */
+    void setJwtSecret(const std::string& secret);
+
+    /**
+     * @brief Get JWT secret
+     *
+     * @return std::string JWT secret key
+     */
+    std::string getJwtSecret() const;
+
+    /**
+     * @brief Set authentication method
+     *
+     * @param method Authentication method
+     */
+    void setAuthMethod(UserManager::AuthMethod method);
+
+    /**
+     * @brief Get authentication method
+     *
+     * @return UserManager::AuthMethod Current authentication method
+     */
+    UserManager::AuthMethod getAuthMethod() const;
+
+    /**
+     * @brief Enable or disable guest access
+     *
+     * @param enable Enable guest access
+     */
+    void setGuestAccessEnabled(bool enable);
+
+    /**
+     * @brief Check if guest access is enabled
+     *
+     * @return true if guest access enabled, false otherwise
+     */
+    bool isGuestAccessEnabled() const;
+
+    /**
+     * @brief Get authentication configuration
+     *
+     * @return AuthConfig Current configuration
+     */
+    AuthConfig getConfig() const;
+
+    /**
+     * @brief Update authentication configuration
+     *
+     * @param config New configuration
+     */
+    void updateConfig(const AuthConfig& config);
+
+private:
+    AuthConfig m_config;                           ///< Authentication configuration
+    std::shared_ptr<UserManager> m_userManager;    ///< User manager instance
+    std::unique_ptr<JWTAuth> m_jwtAuth;            ///< JWT authentication instance
+
+    /**
+     * @brief Authenticate using JWT token
+     *
+     * @param req HTTP request
+     * @return AuthContext Authentication context
+     */
+    AuthContext authenticateJwt(const httplib::Request& req);
+
+    /**
+     * @brief Authenticate using API key
+     *
+     * @param req HTTP request
+     * @return AuthContext Authentication context
+     */
+    AuthContext authenticateApiKey(const httplib::Request& req);
+
+    /**
+     * @brief Authenticate using Unix system
+     *
+     * @param req HTTP request
+     * @return AuthContext Authentication context
+     */
+    AuthContext authenticateUnix(const httplib::Request& req);
+
+    /**
+     * @brief Extract token from request
+     *
+     * @param req HTTP request
+     * @param headerName Header name to check
+     * @return std::string Token string, empty if not found
+     */
+    std::string extractToken(const httplib::Request& req, const std::string& headerName) const;
+
+    /**
+     * @brief Create guest authentication context
+     *
+     * @return AuthContext Guest context
+     */
+    AuthContext createGuestContext() const;
+
+    /**
+     * @brief Check if path matches pattern
+     *
+     * @param path Request path
+     * @param patterns List of patterns to match
+     * @return true if path matches any pattern, false otherwise
+     */
+    static bool pathMatchesPattern(const std::string& path,
+                                  const std::vector<std::string>& patterns);
+
+    /**
+     * @brief Get required permissions for path
+     *
+     * @param path Request path
+     * @return std::vector<std::string> Required permissions
+     */
+    std::vector<std::string> getRequiredPermissions(const std::string& path) const;
+
+    /**
+     * @brief Log authentication attempt
+     *
+     * @param req HTTP request
+     * @param context Authentication context
+     * @param success Authentication success
+     */
+    void logAuthAttempt(const httplib::Request& req,
+                       const AuthContext& context,
+                       bool success) const;
+
+    /**
+     * @brief Get client IP address from request
+     *
+     * @param req HTTP request
+     * @return std::string Client IP address
+     */
+    static std::string getClientIp(const httplib::Request& req);
+
+    /**
+     * @brief Get user agent from request
+     *
+     * @param req HTTP request
+     * @return std::string User agent string
+     */
+    static std::string getUserAgent(const httplib::Request& req);
+
+    /**
+     * @brief Validate authentication configuration
+     *
+     * @param config Configuration to validate
+     * @return true if valid, false otherwise
+     */
+    static bool validateConfig(const AuthConfig& config);
+
+    /**
+     * @brief Initialize default paths
+     */
+    void initializeDefaultPaths();
+
+    /**
+     * @brief Check if authentication is completely disabled
+     *
+     * @return true if authentication disabled, false otherwise
+     */
+    bool isAuthenticationDisabled() const;
+};
+
+/**
+ * @brief Authentication middleware factory functions
+ */
+namespace AuthMiddlewareFactory {
+    /**
+     * @brief Create authentication middleware with default configuration
+     *
+     * @param userManager User manager instance
+     * @param dataDir Data directory for user storage
+     * @return std::unique_ptr<AuthMiddleware> Auth middleware instance
+     */
+    std::unique_ptr<AuthMiddleware> createDefault(std::shared_ptr<UserManager> userManager,
+                                                  const std::string& dataDir);
+
+    /**
+     * @brief Create authentication middleware with JWT only
+     *
+     * @param userManager User manager instance
+     * @param jwtSecret JWT secret key
+     * @param jwtExpirationMinutes JWT expiration in minutes
+     * @return std::unique_ptr<AuthMiddleware> Auth middleware instance
+     */
+    std::unique_ptr<AuthMiddleware> createJwtOnly(std::shared_ptr<UserManager> userManager,
+                                                   const std::string& jwtSecret,
+                                                   int jwtExpirationMinutes = 60);
+
+    /**
+     * @brief Create authentication middleware with API keys only
+     *
+     * @param userManager User manager instance
+     * @return std::unique_ptr<AuthMiddleware> Auth middleware instance
+     */
+    std::unique_ptr<AuthMiddleware> createApiKeyOnly(std::shared_ptr<UserManager> userManager);
+
+    /**
+     * @brief Create authentication middleware with multiple methods
+     *
+     * @param userManager User manager instance
+     * @param config Authentication configuration
+     * @return std::unique_ptr<AuthMiddleware> Auth middleware instance
+     */
+    std::unique_ptr<AuthMiddleware> createMultiMethod(std::shared_ptr<UserManager> userManager,
+                                                      const AuthConfig& config);
+
+    /**
+     * @brief Create authentication middleware for development (no auth required)
+     *
+     * @return std::unique_ptr<AuthMiddleware> Auth middleware instance
+     */
+    std::unique_ptr<AuthMiddleware> createDevelopment();
+};
+
+#endif // AUTH_MIDDLEWARE_H

+ 267 - 0
include/jwt_auth.h

@@ -0,0 +1,267 @@
+#ifndef JWT_AUTH_H
+#define JWT_AUTH_H
+
+#include <string>
+#include <vector>
+#include <map>
+#include <chrono>
+#include <memory>
+
+/**
+ * @brief JWT (JSON Web Token) authentication implementation
+ *
+ * This class provides JWT token generation and validation functionality
+ * for the stable-diffusion.cpp-rest server. It supports HS256 algorithm
+ * and includes claims for user identification, expiration, and roles.
+ */
+class JWTAuth {
+public:
+    /**
+     * @brief JWT token claims structure
+     */
+    struct Claims {
+        std::string userId;        ///< User identifier
+        std::string username;      ///< Username
+        std::string role;          ///< User role (admin, user, etc.)
+        std::vector<std::string> permissions;  ///< User permissions
+        int64_t issuedAt;         ///< Issued at timestamp
+        int64_t expiresAt;        ///< Expiration timestamp
+        std::string issuer;        ///< Token issuer
+        std::string audience;      ///< Token audience
+    };
+
+    /**
+     * @brief Authentication result structure
+     */
+    struct AuthResult {
+        bool success;              ///< Authentication success status
+        std::string userId;        ///< User ID if successful
+        std::string username;      ///< Username if successful
+        std::string role;          ///< User role if successful
+        std::vector<std::string> permissions;  ///< Permissions if successful
+        std::string errorMessage;  ///< Error message if failed
+        std::string errorCode;     ///< Error code for API responses
+    };
+
+    /**
+     * @brief Construct a new JWT Auth object
+     *
+     * @param secret Secret key for signing tokens
+     * @param tokenExpirationMinutes Token expiration time in minutes (default: 60)
+     * @param issuer Token issuer (default: "stable-diffusion-rest")
+     */
+    explicit JWTAuth(const std::string& secret,
+                    int tokenExpirationMinutes = 60,
+                    const std::string& issuer = "stable-diffusion-rest");
+
+    /**
+     * @brief Destroy the JWT Auth object
+     */
+    ~JWTAuth();
+
+    /**
+     * @brief Generate a JWT token for the given user
+     *
+     * @param userId User identifier
+     * @param username Username
+     * @param role User role
+     * @param permissions User permissions list
+     * @return std::string JWT token string, empty on failure
+     */
+    std::string generateToken(const std::string& userId,
+                             const std::string& username,
+                             const std::string& role,
+                             const std::vector<std::string>& permissions = {});
+
+    /**
+     * @brief Validate a JWT token and extract claims
+     *
+     * @param token JWT token string
+     * @return AuthResult Authentication result with user information
+     */
+    AuthResult validateToken(const std::string& token);
+
+    /**
+     * @brief Refresh an existing token (extend expiration)
+     *
+     * @param token Existing JWT token
+     * @return std::string New JWT token, empty on failure
+     */
+    std::string refreshToken(const std::string& token);
+
+    /**
+     * @brief Extract token from Authorization header
+     *
+     * @param authHeader Authorization header value
+     * @return std::string Token string, empty if not found or invalid format
+     */
+    static std::string extractTokenFromHeader(const std::string& authHeader);
+
+    /**
+     * @brief Check if user has required permission
+     *
+     * @param permissions User permissions list
+     * @param requiredPermission Required permission to check
+     * @return true if user has permission, false otherwise
+     */
+    static bool hasPermission(const std::vector<std::string>& permissions,
+                             const std::string& requiredPermission);
+
+    /**
+     * @brief Check if user has any of the required permissions
+     *
+     * @param permissions User permissions list
+     * @param requiredPermissions List of permissions to check (any one is sufficient)
+     * @return true if user has any of the permissions, false otherwise
+     */
+    static bool hasAnyPermission(const std::vector<std::string>& permissions,
+                                const std::vector<std::string>& requiredPermissions);
+
+    /**
+     * @brief Get token expiration time
+     *
+     * @param token JWT token string
+     * @return int64_t Expiration timestamp, 0 on failure
+     */
+    int64_t getTokenExpiration(const std::string& token);
+
+    /**
+     * @brief Check if token is expired
+     *
+     * @param token JWT token string
+     * @return true if token is expired, false otherwise
+     */
+    bool isTokenExpired(const std::string& token);
+
+    /**
+     * @brief Set token expiration time
+     *
+     * @param minutes Expiration time in minutes
+     */
+    void setTokenExpiration(int minutes);
+
+    /**
+     * @brief Get token expiration time in minutes
+     *
+     * @return int Token expiration time in minutes
+     */
+    int getTokenExpiration() const;
+
+    /**
+     * @brief Set issuer for tokens
+     *
+     * @param issuer Issuer string
+     */
+    void setIssuer(const std::string& issuer);
+
+    /**
+     * @brief Get issuer string
+     *
+     * @return std::string Issuer string
+     */
+    std::string getIssuer() const;
+
+    /**
+     * @brief Generate a random API key
+     *
+     * @param length Length of the API key (default: 32)
+     * @return std::string Random API key
+     */
+    static std::string generateApiKey(int length = 32);
+
+    /**
+     * @brief Validate API key format
+     *
+     * @param apiKey API key string
+     * @return true if format is valid, false otherwise
+     */
+    static bool validateApiKeyFormat(const std::string& apiKey);
+
+private:
+    std::string m_secret;                    ///< Secret key for signing
+    int m_tokenExpirationMinutes;            ///< Token expiration in minutes
+    std::string m_issuer;                    ///< Token issuer
+
+    /**
+     * @brief Base64 URL encode a string
+     *
+     * @param input Input string
+     * @return std::string Base64 URL encoded string
+     */
+    static std::string base64UrlEncode(const std::string& input);
+
+    /**
+     * @brief Base64 URL decode a string
+     *
+     * @param input Base64 URL encoded string
+     * @return std::string Decoded string
+     */
+    static std::string base64UrlDecode(const std::string& input);
+
+    /**
+     * @brief Create JWT header
+     *
+     * @return std::string JWT header JSON string
+     */
+    std::string createHeader() const;
+
+    /**
+     * @brief Create JWT payload from claims
+     *
+     * @param claims Token claims
+     * @return std::string JWT payload JSON string
+     */
+    std::string createPayload(const Claims& claims) const;
+
+    /**
+     * @brief Parse JWT payload from token
+     *
+     * @param token JWT token
+     * @return Claims Parsed claims, empty on failure
+     */
+    Claims parsePayload(const std::string& token) const;
+
+    /**
+     * @brief Create HMAC-SHA256 signature
+     *
+     * @param header Payload header
+     * @param payload Payload data
+     * @return std::string Signature string
+     */
+    std::string createSignature(const std::string& header, const std::string& payload) const;
+
+    /**
+     * @brief Verify HMAC-SHA256 signature
+     *
+     * @param header Payload header
+     * @param payload Payload data
+     * @param signature Signature to verify
+     * @return true if signature is valid, false otherwise
+     */
+    bool verifySignature(const std::string& header, const std::string& payload, const std::string& signature) const;
+
+    /**
+     * @brief Split JWT token into parts
+     *
+     * @param token JWT token
+     * @return std::vector<std::string> Token parts (header, payload, signature)
+     */
+    static std::vector<std::string> splitToken(const std::string& token);
+
+    /**
+     * @brief Get current timestamp in seconds
+     *
+     * @return int64_t Current timestamp
+     */
+    static int64_t getCurrentTimestamp();
+
+    /**
+     * @brief Generate random string
+     *
+     * @param length Length of the string
+     * @return std::string Random string
+     */
+    static std::string generateRandomString(int length);
+};
+
+#endif // JWT_AUTH_H

+ 31 - 0
include/server_config.h

@@ -2,6 +2,34 @@
 #define SERVER_CONFIG_H
 
 #include <string>
+#include <vector>
+
+/**
+ * @brief Authentication method enumeration
+ */
+enum class AuthMethod {
+    NONE,           ///< No authentication required
+    JWT,            ///< JWT token authentication
+    API_KEY,        ///< API key authentication
+    UNIX,           ///< Unix system authentication
+    OPTIONAL        ///< Authentication optional (guest access allowed)
+};
+
+/**
+ * @brief Authentication configuration structure
+ */
+struct AuthConfig {
+    AuthMethod authMethod = AuthMethod::NONE;   ///< Primary authentication method
+    bool enableGuestAccess = false;              ///< Allow unauthenticated access
+    std::string jwtSecret = "";                  ///< JWT secret key (auto-generated if empty)
+    int jwtExpirationMinutes = 60;               ///< JWT token expiration in minutes
+    std::string authRealm = "stable-diffusion-rest"; ///< Authentication realm
+    std::string dataDir = "./auth";              ///< Directory for authentication data
+    bool enableUnixAuth = false;                 ///< Enable Unix authentication
+    std::vector<std::string> publicPaths;        ///< Paths that don't require authentication
+    std::vector<std::string> adminPaths;         ///< Paths that require admin access
+    std::vector<std::string> userPaths;          ///< Paths that require user access
+};
 
 // Server configuration structure
 struct ServerConfig {
@@ -37,6 +65,9 @@ struct ServerConfig {
     // Logging options
     bool enableFileLogging = false;
     std::string logFilePath = "/var/log/stable-diffusion-rest/server.log";
+
+    // Authentication configuration
+    AuthConfig auth;
 };
 
 #endif // SERVER_CONFIG_H

+ 475 - 0
include/user_manager.h

@@ -0,0 +1,475 @@
+#ifndef USER_MANAGER_H
+#define USER_MANAGER_H
+
+#include <string>
+#include <vector>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <functional>
+
+/**
+ * @brief User information structure
+ */
+struct UserInfo {
+    std::string id;                    ///< Unique user ID
+    std::string username;              ///< Username (unique)
+    std::string email;                 ///< Email address
+    std::string passwordHash;          ///< Hashed password
+    std::string role;                  ///< User role (admin, user, etc.)
+    std::vector<std::string> permissions;  ///< User permissions
+    std::vector<std::string> apiKeys;  ///< User's API keys
+    bool active;                       ///< Account active status
+    int64_t createdAt;                 ///< Account creation timestamp
+    int64_t lastLoginAt;               ///< Last login timestamp
+    int64_t passwordChangedAt;         ///< Password change timestamp
+    std::string createdBy;             ///< Who created this user
+};
+
+/**
+ * @brief API key information structure
+ */
+struct ApiKeyInfo {
+    std::string keyId;                 ///< Unique key ID
+    std::string keyHash;               ///< Hashed API key
+    std::string name;                  ///< Key name/description
+    std::string userId;                ///< Owner user ID
+    std::vector<std::string> permissions;  ///< Key permissions
+    bool active;                       ///< Key active status
+    int64_t createdAt;                 ///< Key creation timestamp
+    int64_t lastUsedAt;                ///< Last used timestamp
+    int64_t expiresAt;                 ///< Key expiration timestamp (0 = no expiration)
+    std::string createdBy;             ///< Who created this key
+};
+
+/**
+ * @brief Authentication result structure
+ */
+struct AuthResult {
+    bool success;                       ///< Authentication success
+    std::string userId;                 ///< User ID if successful
+    std::string username;               ///< Username if successful
+    std::string role;                   ///< User role if successful
+    std::vector<std::string> permissions;  ///< User permissions if successful
+    std::string errorMessage;           ///< Error message if failed
+    std::string errorCode;              ///< Error code for API responses
+};
+
+/**
+ * @brief User management system
+ *
+ * This class provides user authentication, authorization, and management
+ * functionality for the stable-diffusion.cpp-rest server. It supports
+ * local user storage, API key management, and role-based access control.
+ */
+class UserManager {
+public:
+    /**
+     * @brief Authentication methods enumeration
+     */
+    enum class AuthMethod {
+        NONE,           ///< No authentication required
+        JWT,            ///< JWT token authentication
+        API_KEY,        ///< API key authentication
+        UNIX,           ///< Unix system authentication
+        OPTIONAL        ///< Authentication optional (guest access allowed)
+    };
+
+    /**
+     * @brief User roles enumeration
+     */
+    enum class UserRole {
+        GUEST,          ///< Guest user (no authentication)
+        USER,           ///< Regular user
+        ADMIN,          ///< Administrator
+        SERVICE         ///< Service account
+    };
+
+    /**
+     * @brief Standard permissions
+     */
+    struct Permissions {
+        static const std::string READ;           ///< Read access to models and status
+        static const std::string GENERATE;       ///< Generate images
+        static const std::string QUEUE_MANAGE;   ///< Manage generation queue
+        static const std::string MODEL_MANAGE;   ///< Load/unload models
+        static const std::string USER_MANAGE;    ///< Manage other users
+        static const std::string ADMIN;          ///< Full administrative access
+    };
+
+    /**
+     * @brief Construct a new User Manager object
+     *
+     * @param dataDir Directory for storing user data
+     * @param authMethod Primary authentication method
+     * @param enableUnixAuth Enable Unix authentication
+     */
+    explicit UserManager(const std::string& dataDir,
+                        AuthMethod authMethod = AuthMethod::JWT,
+                        bool enableUnixAuth = false);
+
+    /**
+     * @brief Destroy the User Manager object
+     */
+    ~UserManager();
+
+    /**
+     * @brief Initialize the user manager
+     *
+     * @return true if initialization successful, false otherwise
+     */
+    bool initialize();
+
+    /**
+     * @brief Shutdown the user manager
+     */
+    void shutdown();
+
+    /**
+     * @brief Authenticate user with username and password
+     *
+     * @param username Username
+     * @param password Plain text password
+     * @return AuthResult Authentication result
+     */
+    AuthResult authenticateUser(const std::string& username, const std::string& password);
+
+    /**
+     * @brief authenticate user with Unix system
+     *
+     * @param username Unix username
+     * @return AuthResult Authentication result
+     */
+    AuthResult authenticateUnix(const std::string& username);
+
+    /**
+     * @brief Authenticate with API key
+     *
+     * @param apiKey API key string
+     * @return AuthResult Authentication result
+     */
+    AuthResult authenticateApiKey(const std::string& apiKey);
+
+    /**
+     * @brief Create a new user
+     *
+     * @param username Username
+     * @param password Plain text password
+     * @param email Email address
+     * @param role User role
+     * @param createdBy User ID of creator
+     * @return std::pair<bool, std::string> Success flag and user ID or error message
+     */
+    std::pair<bool, std::string> createUser(const std::string& username,
+                                           const std::string& password,
+                                           const std::string& email,
+                                           UserRole role = UserRole::USER,
+                                           const std::string& createdBy = "system");
+
+    /**
+     * @brief Update user information
+     *
+     * @param userId User ID
+     * @param updates Map of fields to update
+     * @return std::pair<bool, std::string> Success flag and message
+     */
+    std::pair<bool, std::string> updateUser(const std::string& userId,
+                                           const std::map<std::string, std::string>& updates);
+
+    /**
+     * @brief Delete a user
+     *
+     * @param userId User ID to delete
+     * @param requestingUserId User ID making the request
+     * @return std::pair<bool, std::string> Success flag and message
+     */
+    std::pair<bool, std::string> deleteUser(const std::string& userId,
+                                           const std::string& requestingUserId);
+
+    /**
+     * @brief Change user password
+     *
+     * @param userId User ID
+     * @param oldPassword Current password (can be empty for admin)
+     * @param newPassword New password
+     * @param requestingUserId User ID making the request
+     * @return std::pair<bool, std::string> Success flag and message
+     */
+    std::pair<bool, std::string> changePassword(const std::string& userId,
+                                               const std::string& oldPassword,
+                                               const std::string& newPassword,
+                                               const std::string& requestingUserId);
+
+    /**
+     * @brief Get user information
+     *
+     * @param userId User ID
+     * @return UserInfo User information, empty if not found
+     */
+    UserInfo getUserInfo(const std::string& userId);
+
+    /**
+     * @brief Get user information by username
+     *
+     * @param username Username
+     * @return UserInfo User information, empty if not found
+     */
+    UserInfo getUserInfoByUsername(const std::string& username);
+
+    /**
+     * @brief List all users
+     *
+     * @param requestingUserId User ID making the request
+     * @return std::vector<UserInfo> List of users (limited for non-admins)
+     */
+    std::vector<UserInfo> listUsers(const std::string& requestingUserId);
+
+    /**
+     * @brief Create API key for user
+     *
+     * @param userId User ID
+     * @param name Key name/description
+     * @param permissions Key permissions
+     * @param expiresAt Expiration timestamp (0 = no expiration)
+     * @param createdBy User ID creating the key
+     * @return std::pair<bool, std::string> Success flag and API key or error message
+     */
+    std::pair<bool, std::string> createApiKey(const std::string& userId,
+                                              const std::string& name,
+                                              const std::vector<std::string>& permissions,
+                                              int64_t expiresAt = 0,
+                                              const std::string& createdBy = "system");
+
+    /**
+     * @brief Revoke API key
+     *
+     * @param keyId API key ID
+     * @param requestingUserId User ID making the request
+     * @return std::pair<bool, std::string> Success flag and message
+     */
+    std::pair<bool, std::string> revokeApiKey(const std::string& keyId,
+                                             const std::string& requestingUserId);
+
+    /**
+     * @brief List API keys for user
+     *
+     * @param userId User ID
+     * @param requestingUserId User ID making the request
+     * @return std::vector<ApiKeyInfo> List of API keys
+     */
+    std::vector<ApiKeyInfo> listApiKeys(const std::string& userId,
+                                        const std::string& requestingUserId);
+
+    /**
+     * @brief Get API key information
+     *
+     * @param keyId API key ID
+     * @param requestingUserId User ID making the request
+     * @return ApiKeyInfo API key information, empty if not found
+     */
+    ApiKeyInfo getApiKeyInfo(const std::string& keyId,
+                             const std::string& requestingUserId);
+
+    /**
+     * @brief Update API key last used timestamp
+     *
+     * @param keyId API key ID
+     */
+    void updateApiKeyLastUsed(const std::string& keyId);
+
+    /**
+     * @brief Check if user has permission
+     *
+     * @param userId User ID
+     * @param permission Permission to check
+     * @return true if user has permission, false otherwise
+     */
+    bool hasPermission(const std::string& userId, const std::string& permission);
+
+    /**
+     * @brief Check if user has any of the specified permissions
+     *
+     * @param userId User ID
+     * @param permissions List of permissions to check
+     * @return true if user has any permission, false otherwise
+     */
+    bool hasAnyPermission(const std::string& userId,
+                         const std::vector<std::string>& permissions);
+
+    /**
+     * @brief Get user role as string
+     *
+     * @param role User role enum
+     * @return std::string Role string
+     */
+    static std::string roleToString(UserRole role);
+
+    /**
+     * @brief Parse role from string
+     *
+     * @param roleStr Role string
+     * @return UserRole Role enum
+     */
+    static UserRole stringToRole(const std::string& roleStr);
+
+    /**
+     * @brief Get default permissions for role
+     *
+     * @param role User role
+     * @return std::vector<std::string> List of permissions
+     */
+    static std::vector<std::string> getDefaultPermissions(UserRole role);
+
+    /**
+     * @brief Set authentication method
+     *
+     * @param method Authentication method
+     */
+    void setAuthMethod(AuthMethod method);
+
+    /**
+     * @brief Get current authentication method
+     *
+     * @return AuthMethod Current authentication method
+     */
+    AuthMethod getAuthMethod() const;
+
+    /**
+     * @brief Enable or disable Unix authentication
+     *
+     * @param enable Enable Unix authentication
+     */
+    void setUnixAuthEnabled(bool enable);
+
+    /**
+     * @brief Check if Unix authentication is enabled
+     *
+     * @return true if Unix authentication is enabled, false otherwise
+     */
+    bool isUnixAuthEnabled() const;
+
+    /**
+     * @brief Get user statistics
+     *
+     * @return std::map<std::string, int> Statistics map
+     */
+    std::map<std::string, int> getStatistics();
+
+private:
+    std::string m_dataDir;                    ///< Data directory
+    AuthMethod m_authMethod;                  ///< Current auth method
+    bool m_unixAuthEnabled;                   ///< Unix auth enabled flag
+    std::map<std::string, UserInfo> m_users;  ///< User storage (username -> UserInfo)
+    std::map<std::string, ApiKeyInfo> m_apiKeys; ///< API key storage (keyId -> ApiKeyInfo)
+    std::map<std::string, std::string> m_apiKeyMap; ///< API key hash -> keyId mapping
+    mutable std::mutex m_mutex;               ///< Thread safety mutex
+
+    /**
+     * @brief Hash password using bcrypt
+     *
+     * @param password Plain text password
+     * @return std::string Hashed password
+     */
+    std::string hashPassword(const std::string& password);
+
+    /**
+     * @brief Verify password against hash
+     *
+     * @param password Plain text password
+     * @param hash Password hash
+     * @return true if password matches, false otherwise
+     */
+    bool verifyPassword(const std::string& password, const std::string& hash);
+
+    /**
+     * @brief Hash API key
+     *
+     * @param apiKey Plain text API key
+     * @return std::string Hashed API key
+     */
+    std::string hashApiKey(const std::string& apiKey);
+
+    /**
+     * @brief Generate unique user ID
+     *
+     * @return std::string Unique user ID
+     */
+    std::string generateUserId();
+
+    /**
+     * @brief Generate unique API key ID
+     *
+     * @return std::string Unique API key ID
+     */
+    std::string generateKeyId();
+
+    /**
+     * @brief Save user data to file
+     *
+     * @return true if successful, false otherwise
+     */
+    bool saveUserData();
+
+    /**
+     * @brief Load user data from file
+     *
+     * @return true if successful, false otherwise
+     */
+    bool loadUserData();
+
+    /**
+     * @brief Save API key data to file
+     *
+     * @return true if successful, false otherwise
+     */
+    bool saveApiKeyData();
+
+    /**
+     * @brief Load API key data from file
+     *
+     * @return true if successful, false otherwise
+     */
+    bool loadApiKeyData();
+
+    /**
+     * @brief Get current timestamp
+     *
+     * @return int64_t Current timestamp
+     */
+    static int64_t getCurrentTimestamp();
+
+    /**
+     * @brief Validate username format
+     *
+     * @param username Username to validate
+     * @return true if valid, false otherwise
+     */
+    static bool validateUsername(const std::string& username);
+
+    /**
+     * @brief Validate password strength
+     *
+     * @param password Password to validate
+     * @return true if valid, false otherwise
+     */
+    static bool validatePassword(const std::string& password);
+
+    /**
+     * @brief Validate email format
+     *
+     * @param email Email to validate
+     * @return true if valid, false otherwise
+     */
+    static bool validateEmail(const std::string& email);
+
+    /**
+     * @brief Check if user can manage target user
+     *
+     * @param requestingUserId User making request
+     * @param targetUserId Target user
+     * @return true if allowed, false otherwise
+     */
+    bool canManageUser(const std::string& requestingUserId, const std::string& targetUserId);
+};
+
+#endif // USER_MANAGER_H

+ 7 - 0
src/CMakeLists.txt

@@ -9,6 +9,9 @@ set(SOURCES
     generation_queue.cpp
     stable_diffusion_wrapper.cpp
     logger.cpp
+    jwt_auth.cpp
+    user_manager.cpp
+    auth_middleware.cpp
 )
 
 # Collect header files
@@ -18,6 +21,10 @@ set(HEADERS
     ../include/model_detector.h
     ../include/generation_queue.h
     ../include/stable_diffusion_wrapper.h
+    ../include/jwt_auth.h
+    ../include/user_manager.h
+    ../include/auth_middleware.h
+    ../include/server_config.h
 )
 
 # Create the executable target

+ 562 - 0
src/auth_middleware.cpp

@@ -0,0 +1,562 @@
+#include "auth_middleware.h"
+#include <httplib.h>
+#include <nlohmann/json.hpp>
+#include <fstream>
+#include <sstream>
+#include <iomanip>
+#include <algorithm>
+#include <regex>
+
+using json = nlohmann::json;
+
+AuthMiddleware::AuthMiddleware(const AuthConfig& config,
+                             std::shared_ptr<UserManager> userManager)
+    : m_config(config)
+    , m_userManager(userManager)
+{
+}
+
+AuthMiddleware::~AuthMiddleware() = default;
+
+bool AuthMiddleware::initialize() {
+    try {
+        // Validate configuration
+        if (!validateConfig(m_config)) {
+            return false;
+        }
+
+        // Initialize JWT auth if needed
+        if (m_config.authMethod == AuthMethod::JWT) {
+            m_jwtAuth = std::make_unique<JWTAuth>(m_config.jwtSecret,
+                                                  m_config.jwtExpirationMinutes,
+                                                  "stable-diffusion-rest");
+        }
+
+        // Initialize default paths
+        initializeDefaultPaths();
+
+        return true;
+    } catch (const std::exception& e) {
+        return false;
+    }
+}
+
+AuthContext AuthMiddleware::authenticate(const httplib::Request& req, httplib::Response& res) {
+    AuthContext context;
+    context.authenticated = false;
+
+    try {
+        // Check if authentication is completely disabled
+        if (isAuthenticationDisabled()) {
+            context = createGuestContext();
+            context.authenticated = true;
+            return context;
+        }
+
+        // Check if path requires authentication
+        if (!requiresAuthentication(req.path)) {
+            context = createGuestContext();
+            context.authenticated = m_config.enableGuestAccess;
+            return context;
+        }
+
+        // Try different authentication methods based on configuration
+        switch (m_config.authMethod) {
+            case AuthMethod::JWT:
+                context = authenticateJwt(req);
+                break;
+            case AuthMethod::API_KEY:
+                context = authenticateApiKey(req);
+                break;
+            case AuthMethod::UNIX:
+                context = authenticateUnix(req);
+                break;
+            case AuthMethod::OPTIONAL:
+                // Try JWT first, then API key, then allow guest
+                context = authenticateJwt(req);
+                if (!context.authenticated) {
+                    context = authenticateApiKey(req);
+                }
+                if (!context.authenticated && m_config.enableGuestAccess) {
+                    context = createGuestContext();
+                    context.authenticated = true;
+                }
+                break;
+            case AuthMethod::NONE:
+            default:
+                context = createGuestContext();
+                context.authenticated = true;
+                break;
+        }
+
+        // Check if user has required permissions for this path
+        if (context.authenticated && !hasPathAccess(req.path, context.permissions)) {
+            context.authenticated = false;
+            context.errorMessage = "Insufficient permissions for this endpoint";
+            context.errorCode = "INSUFFICIENT_PERMISSIONS";
+        }
+
+        // Log authentication attempt
+        logAuthAttempt(req, context, context.authenticated);
+
+    } catch (const std::exception& e) {
+        context.authenticated = false;
+        context.errorMessage = "Authentication error: " + std::string(e.what());
+        context.errorCode = "AUTH_ERROR";
+    }
+
+    return context;
+}
+
+bool AuthMiddleware::requiresAuthentication(const std::string& path) const {
+    // Check if path is public
+    if (pathMatchesPattern(path, m_config.publicPaths)) {
+        return false;
+    }
+
+    // All other paths require authentication unless auth is completely disabled
+    return !isAuthenticationDisabled();
+}
+
+bool AuthMiddleware::requiresAdminAccess(const std::string& path) const {
+    return pathMatchesPattern(path, m_config.adminPaths);
+}
+
+bool AuthMiddleware::requiresUserAccess(const std::string& path) const {
+    return pathMatchesPattern(path, m_config.userPaths);
+}
+
+bool AuthMiddleware::hasPathAccess(const std::string& path,
+                                  const std::vector<std::string>& permissions) const {
+    // Check admin paths
+    if (requiresAdminAccess(path)) {
+        return JWTAuth::hasPermission(permissions, UserManager::Permissions::ADMIN);
+    }
+
+    // Check user paths
+    if (requiresUserAccess(path)) {
+        return JWTAuth::hasAnyPermission(permissions, {
+            UserManager::Permissions::USER_MANAGE,
+            UserManager::Permissions::ADMIN
+        });
+    }
+
+    // Default: allow access if authenticated
+    return true;
+}
+AuthMiddleware::AuthHandler AuthMiddleware::createMiddleware(AuthHandler handler) {
+    return [this, handler](const httplib::Request& req, httplib::Response& res, const AuthContext& context) {
+        // Authenticate request
+        AuthContext authContext = authenticate(req, res);
+
+
+        // Check if authentication failed
+        if (!authContext.authenticated) {
+            sendAuthError(res, authContext.errorMessage, authContext.errorCode);
+            return;
+        }
+
+
+        // Call the next handler
+        handler(req, res, authContext);
+    };
+}
+void AuthMiddleware::sendAuthError(httplib::Response& res,
+                                  const std::string& message,
+                                  const std::string& errorCode,
+                                  int statusCode) {
+    json error = {
+        {"error", {
+            {"message", message},
+            {"code", errorCode},
+            {"timestamp", std::chrono::duration_cast<std::chrono::seconds>(
+                std::chrono::system_clock::now().time_since_epoch()).count()}
+        }}
+    };
+
+    res.set_header("Content-Type", "application/json");
+    res.set_header("WWW-Authenticate", "Bearer realm=\"" + m_config.authRealm + "\"");
+    res.status = statusCode;
+    res.body = error.dump();
+}
+
+void AuthMiddleware::sendAuthzError(httplib::Response& res,
+                                   const std::string& message,
+                                   const std::string& errorCode) {
+    json error = {
+        {"error", {
+            {"message", message},
+            {"code", errorCode},
+            {"timestamp", std::chrono::duration_cast<std::chrono::seconds>(
+                std::chrono::system_clock::now().time_since_epoch()).count()}
+        }}
+    };
+
+    res.set_header("Content-Type", "application/json");
+    res.status = 403;
+    res.body = error.dump();
+}
+
+void AuthMiddleware::addPublicPath(const std::string& path) {
+    m_config.publicPaths.push_back(path);
+}
+
+void AuthMiddleware::addAdminPath(const std::string& path) {
+    m_config.adminPaths.push_back(path);
+}
+
+void AuthMiddleware::addUserPath(const std::string& path) {
+    m_config.userPaths.push_back(path);
+}
+
+void AuthMiddleware::setJwtSecret(const std::string& secret) {
+    m_config.jwtSecret = secret;
+    if (m_jwtAuth) {
+        m_jwtAuth->setIssuer("stable-diffusion-rest");
+    }
+}
+
+std::string AuthMiddleware::getJwtSecret() const {
+    return m_config.jwtSecret;
+}
+
+void AuthMiddleware::setAuthMethod(UserManager::AuthMethod method) {
+    m_config.authMethod = static_cast<AuthMethod>(method);
+}
+
+UserManager::AuthMethod AuthMiddleware::getAuthMethod() const {
+    return static_cast<UserManager::AuthMethod>(m_config.authMethod);
+}
+
+void AuthMiddleware::setGuestAccessEnabled(bool enable) {
+    m_config.enableGuestAccess = enable;
+}
+
+bool AuthMiddleware::isGuestAccessEnabled() const {
+    return m_config.enableGuestAccess;
+}
+
+AuthConfig AuthMiddleware::getConfig() const {
+    return m_config;
+}
+
+void AuthMiddleware::updateConfig(const AuthConfig& config) {
+    m_config = config;
+    if (m_config.authMethod == AuthMethod::JWT) {
+        m_jwtAuth = std::make_unique<JWTAuth>(m_config.jwtSecret,
+                                              m_config.jwtExpirationMinutes,
+                                              "stable-diffusion-rest");
+    }
+}
+
+AuthContext AuthMiddleware::authenticateJwt(const httplib::Request& req) {
+    AuthContext context;
+    context.authenticated = false;
+
+    if (!m_jwtAuth) {
+        context.errorMessage = "JWT authentication not configured";
+        context.errorCode = "JWT_NOT_CONFIGURED";
+        return context;
+    }
+
+    // Extract token from header
+    std::string token = extractToken(req, "Authorization");
+    if (token.empty()) {
+        context.errorMessage = "Missing authorization token";
+        context.errorCode = "MISSING_TOKEN";
+        return context;
+    }
+
+    // Validate token
+    auto result = m_jwtAuth->validateToken(token);
+    if (!result.success) {
+        context.errorMessage = result.errorMessage;
+        context.errorCode = result.errorCode;
+        return context;
+    }
+
+    // Token is valid
+    context.authenticated = true;
+    context.userId = result.userId;
+    context.username = result.username;
+    context.role = result.role;
+    context.permissions = result.permissions;
+    context.authMethod = "JWT";
+
+    return context;
+}
+
+AuthContext AuthMiddleware::authenticateApiKey(const httplib::Request& req) {
+    AuthContext context;
+    context.authenticated = false;
+
+    if (!m_userManager) {
+        context.errorMessage = "User manager not available";
+        context.errorCode = "USER_MANAGER_UNAVAILABLE";
+        return context;
+    }
+
+    // Extract API key from header
+    std::string apiKey = extractToken(req, "X-API-Key");
+    if (apiKey.empty()) {
+        context.errorMessage = "Missing API key";
+        context.errorCode = "MISSING_API_KEY";
+        return context;
+    }
+
+    // Validate API key
+    auto result = m_userManager->authenticateApiKey(apiKey);
+    if (!result.success) {
+        context.errorMessage = result.errorMessage;
+        context.errorCode = result.errorCode;
+        return context;
+    }
+
+    // API key is valid
+    context.authenticated = true;
+    context.userId = result.userId;
+    context.username = result.username;
+    context.role = result.role;
+    context.permissions = result.permissions;
+    context.authMethod = "API_KEY";
+
+    return context;
+}
+
+AuthContext AuthMiddleware::authenticateUnix(const httplib::Request& req) {
+    AuthContext context;
+    context.authenticated = false;
+
+    if (!m_userManager || !m_userManager->isUnixAuthEnabled()) {
+        context.errorMessage = "Unix authentication not available";
+        context.errorCode = "UNIX_AUTH_UNAVAILABLE";
+        return context;
+    }
+
+    // For Unix auth, we need to get username from request
+    // This could be from a header or client certificate
+    std::string username = req.get_header_value("X-Unix-User");
+    if (username.empty()) {
+        context.errorMessage = "Missing Unix username";
+        context.errorCode = "MISSING_UNIX_USER";
+        return context;
+    }
+
+    // Authenticate Unix user
+    auto result = m_userManager->authenticateUnix(username);
+    if (!result.success) {
+        context.errorMessage = result.errorMessage;
+        context.errorCode = result.errorCode;
+        return context;
+    }
+
+    // Unix authentication successful
+    context.authenticated = true;
+    context.userId = result.userId;
+    context.username = result.username;
+    context.role = result.role;
+    context.permissions = result.permissions;
+    context.authMethod = "UNIX";
+
+    return context;
+}
+
+std::string AuthMiddleware::extractToken(const httplib::Request& req, const std::string& headerName) const {
+    std::string authHeader = req.get_header_value(headerName);
+
+    if (headerName == "Authorization") {
+        return JWTAuth::extractTokenFromHeader(authHeader);
+    }
+
+    return authHeader;
+}
+
+AuthContext AuthMiddleware::createGuestContext() const {
+    AuthContext context;
+    context.authenticated = false;
+    context.userId = "guest";
+    context.username = "guest";
+    context.role = "guest";
+    context.permissions = UserManager::getDefaultPermissions(UserManager::UserRole::GUEST);
+    context.authMethod = "none";
+    return context;
+}
+
+bool AuthMiddleware::pathMatchesPattern(const std::string& path,
+                                          const std::vector<std::string>& patterns) {
+    for (const auto& pattern : patterns) {
+        // Simple exact match for now
+        if (path == pattern) {
+            return true;
+        }
+
+        // Check for prefix match (pattern ends with *)
+        if (pattern.length() > 1 && pattern.back() == '*') {
+            std::string prefix = pattern.substr(0, pattern.length() - 1);
+            if (path.length() >= prefix.length() && path.substr(0, prefix.length()) == prefix) {
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+std::vector<std::string> AuthMiddleware::getRequiredPermissions(const std::string& path) const {
+    if (requiresAdminAccess(path)) {
+        return {UserManager::Permissions::ADMIN};
+    }
+
+    if (requiresUserAccess(path)) {
+        return {UserManager::Permissions::READ};
+    }
+
+    return {};
+}
+
+void AuthMiddleware::logAuthAttempt(const httplib::Request& req,
+                                    const AuthContext& context,
+                                    bool success) const {
+    // In a real implementation, this would log to a file or logging system
+    std::string clientIp = getClientIp(req);
+    std::string userAgent = getUserAgent(req);
+
+    if (success) {
+        // Log successful authentication
+    } else {
+        // Log failed authentication attempt
+    }
+}
+
+std::string AuthMiddleware::getClientIp(const httplib::Request& req) {
+    // Check various headers for client IP
+    std::string ip = req.get_header_value("X-Forwarded-For");
+    if (ip.empty()) {
+        ip = req.get_header_value("X-Real-IP");
+    }
+    if (ip.empty()) {
+        ip = req.get_header_value("X-Client-IP");
+    }
+    if (ip.empty()) {
+        ip = req.remote_addr;
+    }
+    return ip;
+}
+
+std::string AuthMiddleware::getUserAgent(const httplib::Request& req) {
+    return req.get_header_value("User-Agent");
+}
+
+bool AuthMiddleware::validateConfig(const AuthConfig& config) {
+    // Validate JWT configuration
+    if (config.authMethod == AuthMethod::JWT) {
+        if (config.jwtSecret.empty()) {
+            // Will be auto-generated
+        }
+        if (config.jwtExpirationMinutes <= 0 || config.jwtExpirationMinutes > 1440) {
+            return false; // Max 24 hours
+        }
+    }
+
+    // Validate realm
+    if (config.authRealm.empty()) {
+        return false;
+    }
+
+    return true;
+}
+
+void AuthMiddleware::initializeDefaultPaths() {
+    // Add default public paths
+    if (m_config.publicPaths.empty()) {
+        m_config.publicPaths = {
+            "/api/health",
+            "/api/status",
+            "/api/samplers",
+            "/api/schedulers",
+            "/api/parameters",
+            "/api/models",
+            "/api/models/types",
+            "/api/models/directories"
+        };
+    }
+
+    // Add default admin paths
+    if (m_config.adminPaths.empty()) {
+        m_config.adminPaths = {
+            "/api/users",
+            "/api/auth/users",
+            "/api/system/restart"
+        };
+    }
+
+    // Add default user paths
+    if (m_config.userPaths.empty()) {
+        m_config.userPaths = {
+            "/api/generate",
+            "/api/queue",
+            "/api/models/load",
+            "/api/models/unload",
+            "/api/auth/profile",
+            "/api/auth/api-keys"
+        };
+    }
+}
+
+bool AuthMiddleware::isAuthenticationDisabled() const {
+    return m_config.authMethod == AuthMethod::NONE;
+}
+
+// Factory functions
+namespace AuthMiddlewareFactory {
+
+std::unique_ptr<AuthMiddleware> createDefault(std::shared_ptr<UserManager> userManager,
+                                               const std::string& dataDir) {
+    AuthConfig config;
+    config.authMethod = AuthMethod::NONE;
+    config.enableGuestAccess = true;
+    config.jwtSecret = "";
+    config.jwtExpirationMinutes = 60;
+    config.authRealm = "stable-diffusion-rest";
+
+    return std::make_unique<AuthMiddleware>(config, userManager);
+}
+
+std::unique_ptr<AuthMiddleware> createJwtOnly(std::shared_ptr<UserManager> userManager,
+                                               const std::string& jwtSecret,
+                                               int jwtExpirationMinutes) {
+    AuthConfig config;
+    config.authMethod = AuthMethod::JWT;
+    config.enableGuestAccess = false;
+    config.jwtSecret = jwtSecret;
+    config.jwtExpirationMinutes = jwtExpirationMinutes;
+    config.authRealm = "stable-diffusion-rest";
+    config.enableUnixAuth = false;
+
+    return std::make_unique<AuthMiddleware>(config, userManager);
+}
+
+std::unique_ptr<AuthMiddleware> createApiKeyOnly(std::shared_ptr<UserManager> userManager) {
+    AuthConfig config;
+    config.authMethod = AuthMethod::API_KEY;
+    config.enableGuestAccess = false;
+    config.authRealm = "stable-diffusion-rest";
+    config.enableUnixAuth = false;
+
+    return std::make_unique<AuthMiddleware>(config, userManager);
+}
+
+std::unique_ptr<AuthMiddleware> createMultiMethod(std::shared_ptr<UserManager> userManager,
+                                                  const AuthConfig& config) {
+    return std::make_unique<AuthMiddleware>(config, userManager);
+}
+
+std::unique_ptr<AuthMiddleware> createDevelopment() {
+    AuthConfig config;
+    config.authMethod = AuthMethod::NONE;
+    config.enableGuestAccess = true;
+    config.authRealm = "stable-diffusion-rest";
+
+    return std::make_unique<AuthMiddleware>(config, nullptr);
+}
+
+} // namespace AuthMiddlewareFactory

+ 16 - 8
src/generation_queue.cpp

@@ -158,7 +158,7 @@ public:
             if (activeJobs.find(request.id) != activeJobs.end()) {
                 activeJobs[request.id].status = result.success ? GenerationStatus::COMPLETED : GenerationStatus::FAILED;
                 activeJobs[request.id].endTime = endTime;
-                
+
                 // Set final progress to 100% if successful
                 if (result.success) {
                     activeJobs[request.id].progress = 1.0f;
@@ -204,7 +204,7 @@ public:
             it->second.currentStep = step;
             it->second.totalSteps = totalSteps;
             it->second.timeElapsed = static_cast<int64_t>(timeElapsed);
-            
+
             // Calculate time remaining and speed
             if (step > 0 && timeElapsed > 0) {
                 double avgStepTime = static_cast<double>(timeElapsed) / step;
@@ -212,7 +212,7 @@ public:
                 it->second.timeRemaining = static_cast<int64_t>(avgStepTime * remainingSteps);
                 it->second.speed = 1000.0 / avgStepTime; // steps per second
             }
-            
+
             // Save progress to file periodically (every 10 steps or on significant progress changes)
             if (step % 10 == 0 || progress >= 0.99f) {
                 saveJobToFile(it->second);
@@ -296,14 +296,20 @@ public:
             std::vector<StableDiffusionWrapper::GeneratedImage> generatedImages;
 
             // Create progress callback that updates job info
-            auto progressCallback = [this, jobId](int step, int totalSteps, float progress, uint64_t timeElapsed) -> bool {
+            auto progressCallback = [this, jobId](int step, int totalSteps, float progress, void* userData) {
+                // Calculate time elapsed from start time (stored in userData)
+                auto startTime = userData ? *static_cast<std::chrono::steady_clock::time_point*>(userData) : std::chrono::steady_clock::now();
+                auto currentTime = std::chrono::steady_clock::now();
+                uint64_t timeElapsed = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - startTime).count();
                 updateJobProgress(jobId, step, totalSteps, progress, timeElapsed);
-                return true; // Continue generation
             };
 
+            // Store start time to pass as user data
+            auto generationStartTime = std::chrono::steady_clock::now();
+
             switch (request.requestType) {
                 case GenerationRequest::RequestType::TEXT2IMG:
-                    generatedImages = modelWrapper->generateImage(params, progressCallback);
+                    generatedImages = modelWrapper->generateImage(params, progressCallback, &generationStartTime);
                     break;
 
                 case GenerationRequest::RequestType::IMG2IMG:
@@ -316,7 +322,8 @@ public:
                         request.initImageData,
                         request.initImageWidth,
                         request.initImageHeight,
-                        progressCallback
+                        progressCallback,
+                        &generationStartTime
                     );
                     break;
 
@@ -330,7 +337,8 @@ public:
                         request.controlImageData,
                         request.controlImageWidth,
                         request.controlImageHeight,
-                        progressCallback
+                        progressCallback,
+                        &generationStartTime
                     );
                     break;
 

+ 370 - 0
src/jwt_auth.cpp

@@ -0,0 +1,370 @@
+#include "jwt_auth.h"
+#include <openssl/hmac.h>
+#include <openssl/sha.h>
+#include <openssl/rand.h>
+#include <nlohmann/json.hpp>
+#include <sstream>
+#include <iomanip>
+#include <algorithm>
+#include <cctype>
+
+using json = nlohmann::json;
+
+JWTAuth::JWTAuth(const std::string& secret,
+                 int tokenExpirationMinutes,
+                 const std::string& issuer)
+    : m_secret(secret)
+    , m_tokenExpirationMinutes(tokenExpirationMinutes)
+    , m_issuer(issuer)
+{
+    // Generate random secret if not provided
+    if (m_secret.empty()) {
+        m_secret = generateRandomString(64);
+    }
+}
+
+JWTAuth::~JWTAuth() = default;
+
+std::string JWTAuth::generateToken(const std::string& userId,
+                                  const std::string& username,
+                                  const std::string& role,
+                                  const std::vector<std::string>& permissions) {
+    try {
+        // Create claims
+        Claims claims;
+        claims.userId = userId;
+        claims.username = username;
+        claims.role = role;
+        claims.permissions = permissions;
+        claims.issuedAt = getCurrentTimestamp();
+        claims.expiresAt = claims.issuedAt + (m_tokenExpirationMinutes * 60);
+        claims.issuer = m_issuer;
+        claims.audience = "stable-diffusion-rest";
+
+        // Create header and payload
+        std::string header = createHeader();
+        std::string payload = createPayload(claims);
+
+        // Create signature
+        std::string signature = createSignature(header, payload);
+
+        // Combine parts
+        return header + "." + payload + "." + signature;
+    } catch (const std::exception& e) {
+        return "";
+    }
+}
+
+JWTAuth::AuthResult JWTAuth::validateToken(const std::string& token) {
+    AuthResult result;
+    result.success = false;
+
+    try {
+        // Split token
+        auto parts = splitToken(token);
+        if (parts.size() != 3) {
+            result.errorMessage = "Invalid token format";
+            result.errorCode = "INVALID_TOKEN_FORMAT";
+            return result;
+        }
+
+        const std::string& header = parts[0];
+        const std::string& payload = parts[1];
+        const std::string& signature = parts[2];
+
+        // Verify signature
+        if (!verifySignature(header, payload, signature)) {
+            result.errorMessage = "Invalid token signature";
+            result.errorCode = "INVALID_SIGNATURE";
+            return result;
+        }
+
+        // Parse payload
+        Claims claims = parsePayload(token);
+        if (claims.userId.empty()) {
+            result.errorMessage = "Invalid token payload";
+            result.errorCode = "INVALID_PAYLOAD";
+            return result;
+        }
+
+        // Check expiration
+        if (getCurrentTimestamp() >= claims.expiresAt) {
+            result.errorMessage = "Token has expired";
+            result.errorCode = "TOKEN_EXPIRED";
+            return result;
+        }
+
+        // Check issuer
+        if (!claims.issuer.empty() && claims.issuer != m_issuer) {
+            result.errorMessage = "Invalid token issuer";
+            result.errorCode = "INVALID_ISSUER";
+            return result;
+        }
+
+        // Token is valid
+        result.success = true;
+        result.userId = claims.userId;
+        result.username = claims.username;
+        result.role = claims.role;
+        result.permissions = claims.permissions;
+
+    } catch (const std::exception& e) {
+        result.errorMessage = "Token validation failed: " + std::string(e.what());
+        result.errorCode = "VALIDATION_ERROR";
+    }
+
+    return result;
+}
+
+std::string JWTAuth::refreshToken(const std::string& token) {
+    try {
+        // Validate current token
+        AuthResult result = validateToken(token);
+        if (!result.success) {
+            return "";
+        }
+
+        // Generate new token with same claims
+        return generateToken(result.userId, result.username, result.role, result.permissions);
+    } catch (const std::exception& e) {
+        return "";
+    }
+}
+
+std::string JWTAuth::extractTokenFromHeader(const std::string& authHeader) {
+    if (authHeader.empty()) {
+        return "";
+    }
+
+    // Check for "Bearer " prefix
+    const std::string bearerPrefix = "Bearer ";
+    if (authHeader.length() > bearerPrefix.length() &&
+        authHeader.substr(0, bearerPrefix.length()) == bearerPrefix) {
+        return authHeader.substr(bearerPrefix.length());
+    }
+
+    return "";
+}
+
+bool JWTAuth::hasPermission(const std::vector<std::string>& permissions,
+                           const std::string& requiredPermission) {
+    return std::find(permissions.begin(), permissions.end(), requiredPermission) != permissions.end();
+}
+
+bool JWTAuth::hasAnyPermission(const std::vector<std::string>& permissions,
+                               const std::vector<std::string>& requiredPermissions) {
+    for (const auto& permission : requiredPermissions) {
+        if (hasPermission(permissions, permission)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+int64_t JWTAuth::getTokenExpiration(const std::string& token) {
+    try {
+        Claims claims = parsePayload(token);
+        return claims.expiresAt;
+    } catch (const std::exception& e) {
+        return 0;
+    }
+}
+
+bool JWTAuth::isTokenExpired(const std::string& token) {
+    int64_t expiration = getTokenExpiration(token);
+    return expiration > 0 && getCurrentTimestamp() >= expiration;
+}
+
+void JWTAuth::setTokenExpiration(int minutes) {
+    m_tokenExpirationMinutes = minutes;
+}
+
+int JWTAuth::getTokenExpiration() const {
+    return m_tokenExpirationMinutes;
+}
+
+void JWTAuth::setIssuer(const std::string& issuer) {
+    m_issuer = issuer;
+}
+
+std::string JWTAuth::getIssuer() const {
+    return m_issuer;
+}
+
+std::string JWTAuth::generateApiKey(int length) {
+    return generateRandomString(length);
+}
+
+bool JWTAuth::validateApiKeyFormat(const std::string& apiKey) {
+    if (apiKey.length() < 16 || apiKey.length() > 128) {
+        return false;
+    }
+
+    // Check for alphanumeric characters only
+    for (char c : apiKey) {
+        if (!std::isalnum(c)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+std::string JWTAuth::base64UrlEncode(const std::string& input) {
+    const std::string base64Chars =
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+
+    std::string result;
+    int val = 0, valb = -6;
+
+    for (unsigned char c : input) {
+        val = (val << 8) + c;
+        valb += 8;
+        while (valb >= 0) {
+            result.push_back(base64Chars[(val >> valb) & 0x3F]);
+            valb -= 6;
+        }
+    }
+
+    if (valb > -6) {
+        result.push_back(base64Chars[((val << 8) >> (valb + 8)) & 0x3F]);
+    }
+
+    return result;
+}
+
+std::string JWTAuth::base64UrlDecode(const std::string& input) {
+    const std::string base64Chars =
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+
+    std::string result;
+    int val = 0, valb = -8;
+
+    for (char c : input) {
+        if (c == '=') continue;
+
+        size_t pos = base64Chars.find(c);
+        if (pos == std::string::npos) return "";
+
+        val = (val << 6) + pos;
+        valb += 6;
+
+        if (valb >= 0) {
+            result.push_back(char((val >> valb) & 0xFF));
+            valb -= 8;
+        }
+    }
+
+    return result;
+}
+
+std::string JWTAuth::createHeader() const {
+    json header = {
+        {"alg", "HS256"},
+        {"typ", "JWT"}
+    };
+    return base64UrlEncode(header.dump());
+}
+
+std::string JWTAuth::createPayload(const Claims& claims) const {
+    json payload = {
+        {"sub", claims.userId},
+        {"username", claims.username},
+        {"role", claims.role},
+        {"iat", claims.issuedAt},
+        {"exp", claims.expiresAt},
+        {"iss", claims.issuer},
+        {"aud", claims.audience}
+    };
+
+    // Add permissions if not empty
+    if (!claims.permissions.empty()) {
+        payload["permissions"] = claims.permissions;
+    }
+
+    return base64UrlEncode(payload.dump());
+}
+
+JWTAuth::Claims JWTAuth::parsePayload(const std::string& token) const {
+    Claims claims;
+
+    try {
+        auto parts = splitToken(token);
+        if (parts.size() != 3) {
+            return claims;
+        }
+
+        std::string payloadStr = base64UrlDecode(parts[1]);
+        json payload = json::parse(payloadStr);
+
+        claims.userId = payload.value("sub", "");
+        claims.username = payload.value("username", "");
+        claims.role = payload.value("role", "");
+        claims.issuedAt = payload.value("iat", 0);
+        claims.expiresAt = payload.value("exp", 0);
+        claims.issuer = payload.value("iss", "");
+        claims.audience = payload.value("aud", "");
+
+        if (payload.contains("permissions") && payload["permissions"].is_array()) {
+            for (const auto& perm : payload["permissions"]) {
+                claims.permissions.push_back(perm.get<std::string>());
+            }
+        }
+
+    } catch (const std::exception& e) {
+        // Return empty claims on error
+    }
+
+    return claims;
+}
+
+std::string JWTAuth::createSignature(const std::string& header, const std::string& payload) const {
+    std::string data = header + "." + payload;
+
+    unsigned char* digest = HMAC(EVP_sha256(),
+                                 m_secret.c_str(), m_secret.length(),
+                                 (unsigned char*)data.c_str(), data.length(),
+                                 nullptr, nullptr);
+
+    return base64UrlEncode(std::string((char*)digest, SHA256_DIGEST_LENGTH));
+}
+
+bool JWTAuth::verifySignature(const std::string& header, const std::string& payload, const std::string& signature) const {
+    std::string expectedSignature = createSignature(header, payload);
+    return expectedSignature == signature;
+}
+
+std::vector<std::string> JWTAuth::splitToken(const std::string& token) {
+    std::vector<std::string> parts;
+    std::stringstream ss(token);
+    std::string part;
+
+    while (std::getline(ss, part, '.')) {
+        parts.push_back(part);
+    }
+
+    return parts;
+}
+
+int64_t JWTAuth::getCurrentTimestamp() {
+    return std::chrono::duration_cast<std::chrono::seconds>(
+        std::chrono::system_clock::now().time_since_epoch()).count();
+}
+
+std::string JWTAuth::generateRandomString(int length) {
+    const std::string chars =
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+    std::string result;
+    result.reserve(length);
+
+    for (int i = 0; i < length; ++i) {
+        unsigned char randomByte;
+        if (RAND_bytes(&randomByte, 1) != 1) {
+            return "";
+        }
+        result += chars[randomByte % chars.length()];
+    }
+
+    return result;
+}

+ 77 - 0
src/main.cpp

@@ -12,6 +12,8 @@
 #include "generation_queue.h"
 #include "server_config.h"
 #include "logger.h"
+#include "user_manager.h"
+#include "auth_middleware.h"
 
 // Global flag for signal handling
 std::atomic<bool> g_running(true);
@@ -119,6 +121,32 @@ ServerConfig parseArguments(int argc, char* argv[]) {
             config.logFilePath = argv[++i];
         } else if (arg == "--enable-file-logging") {
             config.enableFileLogging = true;
+        } else if (arg == "--auth-method" && i + 1 < argc) {
+            std::string method = argv[++i];
+            if (method == "none") {
+                config.auth.authMethod = AuthMethod::NONE;
+            } else if (method == "jwt") {
+                config.auth.authMethod = AuthMethod::JWT;
+            } else if (method == "api-key") {
+                config.auth.authMethod = AuthMethod::API_KEY;
+            } else if (method == "unix") {
+                config.auth.authMethod = AuthMethod::UNIX;
+            } else if (method == "optional") {
+                config.auth.authMethod = AuthMethod::OPTIONAL;
+            } else {
+                std::cerr << "Invalid auth method: " << method << std::endl;
+                exit(1);
+            }
+        } else if (arg == "--jwt-secret" && i + 1 < argc) {
+            config.auth.jwtSecret = argv[++i];
+        } else if (arg == "--jwt-expiration" && i + 1 < argc) {
+            config.auth.jwtExpirationMinutes = std::stoi(argv[++i]);
+        } else if (arg == "--enable-guest-access") {
+            config.auth.enableGuestAccess = true;
+        } else if (arg == "--enable-unix-auth") {
+            config.auth.enableUnixAuth = true;
+        } else if (arg == "--auth-data-dir" && i + 1 < argc) {
+            config.auth.dataDir = argv[++i];
         } else if (arg == "--help" || arg == "-h") {
             std::cout << "stable-diffusion.cpp-rest server\n"
                       << "Usage: " << argv[0] << " [options]\n\n"
@@ -136,6 +164,14 @@ ServerConfig parseArguments(int argc, char* argv[]) {
                       << "  --enable-file-logging          Enable logging to file\n"
                       << "  --log-file <path>              Log file path (default: /var/log/stable-diffusion-rest/server.log)\n"
                       << "\n"
+                      << "Authentication Options:\n"
+                      << "  --auth-method <method>         Authentication method (none, jwt, api-key, unix, optional)\n"
+                      << "  --jwt-secret <secret>           JWT secret key (auto-generated if not provided)\n"
+                      << "  --jwt-expiration <minutes>      JWT token expiration time (default: 60)\n"
+                      << "  --enable-guest-access          Allow unauthenticated guest access\n"
+                      << "  --enable-unix-auth             Enable Unix system authentication\n"
+                      << "  --auth-data-dir <dir>          Directory for authentication data (default: ./auth)\n"
+                      << "\n"
                       << "Model Directory Options:\n"
                       << "  All model directories are optional and default to standard folder names\n"
                       << "  under --models-dir. Only specify these if your folder names differ.\n"
@@ -317,6 +353,21 @@ int main(int argc, char* argv[]) {
     validateDirectory(config.taesdDir, "TAESD", false);
     validateDirectory(config.vaeDir, "VAE", false);
 
+    // Validate UI directory if specified
+    if (!config.uiDir.empty()) {
+        if (!validateDirectory(config.uiDir, "Web UI", false)) {
+            std::cerr << "\nError: Web UI directory is invalid" << std::endl;
+            return 1;
+        }
+
+        // Check if the UI directory is readable
+        std::filesystem::path uiPath(config.uiDir);
+        if (!std::filesystem::exists(uiPath / "index.html") &&
+            !std::filesystem::exists(uiPath / "index.htm")) {
+            std::cerr << "Warning: Web UI directory does not contain an index.html or index.htm file: " << config.uiDir << std::endl;
+        }
+    }
+
     if (!allValid) {
         std::cerr << "\nError: Base models directory is invalid or missing" << std::endl;
         return 1;
@@ -327,6 +378,32 @@ int main(int argc, char* argv[]) {
     signal(SIGTERM, signalHandler);  // Termination signal
 
     try {
+        // Initialize authentication system
+        if (config.verbose) {
+            std::cout << "Initializing authentication system..." << std::endl;
+        }
+
+        auto userManager = std::make_shared<UserManager>(config.auth.dataDir,
+                                                           static_cast<UserManager::AuthMethod>(config.auth.authMethod),
+                                                           false);
+        if (!userManager->initialize()) {
+            std::cerr << "Error: Failed to initialize user manager" << std::endl;
+            return 1;
+        }
+
+        if (config.verbose) {
+            std::cout << "User manager initialized" << std::endl;
+            std::cout << "Authentication method: ";
+            switch (config.auth.authMethod) {
+                case AuthMethod::NONE: std::cout << "None"; break;
+                case AuthMethod::JWT: std::cout << "JWT"; break;
+                case AuthMethod::API_KEY: std::cout << "API Key"; break;
+                case AuthMethod::UNIX: std::cout << "Unix"; break;
+                case AuthMethod::OPTIONAL: std::cout << "Optional"; break;
+            }
+            std::cout << std::endl;
+        }
+
         // Initialize components
         auto modelManager = std::make_unique<ModelManager>();
         auto generationQueue = std::make_unique<GenerationQueue>(modelManager.get(), config.maxConcurrentGenerations,

+ 982 - 0
src/user_manager.cpp

@@ -0,0 +1,982 @@
+#include "user_manager.h"
+#include "jwt_auth.h"
+#include <nlohmann/json.hpp>
+#include <fstream>
+#include <filesystem>
+#include <sstream>
+#include <iomanip>
+#include <algorithm>
+#include <regex>
+#include <crypt.h>
+#include <unistd.h>
+#include <pwd.h>
+#include <shadow.h>
+#include <sys/types.h>
+#include <openssl/sha.h>
+
+using json = nlohmann::json;
+
+// Define static permission constants
+const std::string UserManager::Permissions::READ = "read";
+const std::string UserManager::Permissions::GENERATE = "generate";
+const std::string UserManager::Permissions::QUEUE_MANAGE = "queue_manage";
+const std::string UserManager::Permissions::MODEL_MANAGE = "model_manage";
+const std::string UserManager::Permissions::USER_MANAGE = "user_manage";
+const std::string UserManager::Permissions::ADMIN = "admin";
+
+UserManager::UserManager(const std::string& dataDir,
+                         AuthMethod authMethod,
+                         bool enableUnixAuth)
+    : m_dataDir(dataDir)
+    , m_authMethod(authMethod)
+    , m_unixAuthEnabled(enableUnixAuth)
+{
+}
+
+UserManager::~UserManager() {
+    shutdown();
+}
+
+bool UserManager::initialize() {
+    try {
+        // Create data directory if it doesn't exist
+        std::filesystem::create_directories(m_dataDir);
+
+        // Load existing user data
+        if (!loadUserData()) {
+            // Create default admin user if no users exist
+            if (m_users.empty()) {
+                auto [success, adminId] = createUser("admin", "admin123", "admin@localhost", UserRole::ADMIN, "system");
+                if (!success) {
+                    return false;
+                }
+            }
+        }
+
+        // Load existing API key data
+        loadApiKeyData();
+
+        return true;
+    } catch (const std::exception& e) {
+        return false;
+    }
+}
+
+void UserManager::shutdown() {
+    // Save data before shutdown
+    saveUserData();
+    saveApiKeyData();
+}
+
+AuthResult UserManager::authenticateUser(const std::string& username, const std::string& password) {
+    AuthResult result;
+    result.success = false;
+
+    try {
+        // Find user by username
+        auto it = m_users.find(username);
+        if (it == m_users.end()) {
+            result.errorMessage = "User not found";
+            result.errorCode = "USER_NOT_FOUND";
+            return result;
+        }
+
+        const UserInfo& user = it->second;
+
+        // Check if user is active
+        if (!user.active) {
+            result.errorMessage = "User account is disabled";
+            result.errorCode = "ACCOUNT_DISABLED";
+            return result;
+        }
+
+        // Verify password
+        if (!verifyPassword(password, user.passwordHash)) {
+            result.errorMessage = "Invalid password";
+            result.errorCode = "INVALID_PASSWORD";
+            return result;
+        }
+
+        // Authentication successful
+        result.success = true;
+        result.userId = user.id;
+        result.username = user.username;
+        result.role = user.role;
+        result.permissions = user.permissions;
+
+        // Update last login time
+        m_users[username].lastLoginAt = getCurrentTimestamp();
+        saveUserData();
+
+    } catch (const std::exception& e) {
+        result.errorMessage = "Authentication failed: " + std::string(e.what());
+        result.errorCode = "AUTH_ERROR";
+    }
+
+    return result;
+}
+
+AuthResult UserManager::authenticateUnix(const std::string& username) {
+    AuthResult result;
+    result.success = false;
+
+    if (!m_unixAuthEnabled) {
+        result.errorMessage = "Unix authentication is disabled";
+        result.errorCode = "UNIX_AUTH_DISABLED";
+        return result;
+    }
+
+    try {
+        // Get user information from system
+        struct passwd* pw = getpwnam(username.c_str());
+        if (!pw) {
+            result.errorMessage = "Unix user not found";
+            result.errorCode = "UNIX_USER_NOT_FOUND";
+            return result;
+        }
+
+        // Check if user exists in our system or create guest user
+        UserInfo user;
+        auto it = m_users.find(username);
+        if (it != m_users.end()) {
+            user = it->second;
+        } else {
+            // Create guest user for Unix authentication
+            user.id = generateUserId();
+            user.username = username;
+            user.email = username + "@localhost";
+            user.role = roleToString(UserRole::USER);
+            user.permissions = getDefaultPermissions(UserRole::USER);
+            user.active = true;
+            user.createdAt = getCurrentTimestamp();
+            user.createdBy = "system";
+
+            m_users[username] = user;
+            saveUserData();
+        }
+
+        // Authentication successful
+        result.success = true;
+        result.userId = user.id;
+        result.username = user.username;
+        result.role = user.role;
+        result.permissions = user.permissions;
+
+    } catch (const std::exception& e) {
+        result.errorMessage = "Unix authentication failed: " + std::string(e.what());
+        result.errorCode = "UNIX_AUTH_ERROR";
+    }
+
+    return result;
+}
+
+AuthResult UserManager::authenticateApiKey(const std::string& apiKey) {
+    AuthResult result;
+    result.success = false;
+
+    try {
+        // Hash the API key to compare with stored hashes
+        std::string keyHash = hashApiKey(apiKey);
+
+        auto it = m_apiKeyMap.find(keyHash);
+        if (it == m_apiKeyMap.end()) {
+            result.errorMessage = "Invalid API key";
+            result.errorCode = "INVALID_API_KEY";
+            return result;
+        }
+
+        const std::string& keyId = it->second;
+        const ApiKeyInfo& keyInfo = m_apiKeys[keyId];
+
+        // Check if key is active
+        if (!keyInfo.active) {
+            result.errorMessage = "API key is disabled";
+            result.errorCode = "API_KEY_DISABLED";
+            return result;
+        }
+
+        // Check expiration
+        if (keyInfo.expiresAt > 0 && getCurrentTimestamp() >= keyInfo.expiresAt) {
+            result.errorMessage = "API key has expired";
+            result.errorCode = "API_KEY_EXPIRED";
+            return result;
+        }
+
+        // Get user information
+        auto userIt = m_users.find(keyInfo.userId);
+        if (userIt == m_users.end()) {
+            result.errorMessage = "API key owner not found";
+            result.errorCode = "USER_NOT_FOUND";
+            return result;
+        }
+
+        const UserInfo& user = userIt->second;
+        if (!user.active) {
+            result.errorMessage = "API key owner account is disabled";
+            result.errorCode = "ACCOUNT_DISABLED";
+            return result;
+        }
+
+        // Authentication successful
+        result.success = true;
+        result.userId = user.id;
+        result.username = user.username;
+        result.role = user.role;
+        result.permissions = keyInfo.permissions.empty() ? user.permissions : keyInfo.permissions;
+
+        // Update last used timestamp
+        m_apiKeys[keyId].lastUsedAt = getCurrentTimestamp();
+        saveApiKeyData();
+
+    } catch (const std::exception& e) {
+        result.errorMessage = "API key authentication failed: " + std::string(e.what());
+        result.errorCode = "API_KEY_AUTH_ERROR";
+    }
+
+    return result;
+}
+
+std::pair<bool, std::string> UserManager::createUser(const std::string& username,
+                                                     const std::string& password,
+                                                     const std::string& email,
+                                                     UserRole role,
+                                                     const std::string& createdBy) {
+    try {
+        // Validate inputs
+        if (!validateUsername(username)) {
+            return {false, "Invalid username format"};
+        }
+
+        if (!validatePassword(password)) {
+            return {false, "Password does not meet requirements"};
+        }
+
+        if (!validateEmail(email)) {
+            return {false, "Invalid email format"};
+        }
+
+        // Check if user already exists
+        if (m_users.find(username) != m_users.end()) {
+            return {false, "User already exists"};
+        }
+
+        // Create user
+        UserInfo user;
+        user.id = generateUserId();
+        user.username = username;
+        user.email = email;
+        user.passwordHash = hashPassword(password);
+        user.role = roleToString(role);
+        user.permissions = getDefaultPermissions(role);
+        user.active = true;
+        user.createdAt = getCurrentTimestamp();
+        user.passwordChangedAt = getCurrentTimestamp();
+        user.createdBy = createdBy;
+
+        m_users[username] = user;
+        saveUserData();
+
+        return {true, user.id};
+
+    } catch (const std::exception& e) {
+        return {false, "Failed to create user: " + std::string(e.what())};
+    }
+}
+
+std::pair<bool, std::string> UserManager::updateUser(const std::string& userId,
+                                                     const std::map<std::string, std::string>& updates) {
+    try {
+        // Find user by ID
+        std::string username;
+        for (const auto& pair : m_users) {
+            if (pair.second.id == userId) {
+                username = pair.first;
+                break;
+            }
+        }
+
+        if (username.empty()) {
+            return {false, "User not found"};
+        }
+
+        UserInfo& user = m_users[username];
+
+        // Update allowed fields
+        for (const auto& update : updates) {
+            const std::string& field = update.first;
+            const std::string& value = update.second;
+
+            if (field == "email") {
+                if (!validateEmail(value)) {
+                    return {false, "Invalid email format"};
+                }
+                user.email = value;
+            } else if (field == "role") {
+                user.role = value;
+                user.permissions = getDefaultPermissions(stringToRole(value));
+            } else if (field == "active") {
+                user.active = (value == "true" || value == "1");
+            }
+        }
+
+        saveUserData();
+        return {true, "User updated successfully"};
+
+    } catch (const std::exception& e) {
+        return {false, "Failed to update user: " + std::string(e.what())};
+    }
+}
+
+std::pair<bool, std::string> UserManager::deleteUser(const std::string& userId,
+                                                     const std::string& requestingUserId) {
+    try {
+        // Find user by ID
+        std::string username;
+        for (const auto& pair : m_users) {
+            if (pair.second.id == userId) {
+                username = pair.first;
+                break;
+            }
+        }
+
+        if (username.empty()) {
+            return {false, "User not found"};
+        }
+
+        // Check permissions
+        if (!canManageUser(requestingUserId, userId)) {
+            return {false, "Insufficient permissions to delete user"};
+        }
+
+        // Delete user's API keys
+        auto it = m_apiKeys.begin();
+        while (it != m_apiKeys.end()) {
+            if (it->second.userId == userId) {
+                m_apiKeyMap.erase(it->second.keyHash);
+                it = m_apiKeys.erase(it);
+            } else {
+                ++it;
+            }
+        }
+
+        // Delete user
+        m_users.erase(username);
+        saveUserData();
+        saveApiKeyData();
+
+        return {true, "User deleted successfully"};
+
+    } catch (const std::exception& e) {
+        return {false, "Failed to delete user: " + std::string(e.what())};
+    }
+}
+
+std::pair<bool, std::string> UserManager::changePassword(const std::string& userId,
+                                                          const std::string& oldPassword,
+                                                          const std::string& newPassword,
+                                                          const std::string& requestingUserId) {
+    try {
+        // Find user by ID
+        std::string username;
+        for (const auto& pair : m_users) {
+            if (pair.second.id == userId) {
+                username = pair.first;
+                break;
+            }
+        }
+
+        if (username.empty()) {
+            return {false, "User not found"};
+        }
+
+        UserInfo& user = m_users[username];
+
+        // Check permissions (admin can change without old password)
+        if (requestingUserId != userId) {
+            if (!canManageUser(requestingUserId, userId)) {
+                return {false, "Insufficient permissions to change password"};
+            }
+        } else {
+            // User changing own password - verify old password
+            if (!verifyPassword(oldPassword, user.passwordHash)) {
+                return {false, "Current password is incorrect"};
+            }
+        }
+
+        // Validate new password
+        if (!validatePassword(newPassword)) {
+            return {false, "New password does not meet requirements"};
+        }
+
+        // Update password
+        user.passwordHash = hashPassword(newPassword);
+        user.passwordChangedAt = getCurrentTimestamp();
+        saveUserData();
+
+        return {true, "Password changed successfully"};
+
+    } catch (const std::exception& e) {
+        return {false, "Failed to change password: " + std::string(e.what())};
+    }
+}
+
+UserInfo UserManager::getUserInfo(const std::string& userId) {
+    for (const auto& pair : m_users) {
+        if (pair.second.id == userId) {
+            return pair.second;
+        }
+    }
+    return UserInfo{};
+}
+
+UserInfo UserManager::getUserInfoByUsername(const std::string& username) {
+    auto it = m_users.find(username);
+    if (it != m_users.end()) {
+        return it->second;
+    }
+    return UserInfo{};
+}
+
+std::vector<UserInfo> UserManager::listUsers(const std::string& requestingUserId) {
+    std::vector<UserInfo> users;
+
+    // Check if requester is admin
+    UserInfo requester = getUserInfo(requestingUserId);
+    bool isAdmin = (requester.role == roleToString(UserRole::ADMIN));
+
+    for (const auto& pair : m_users) {
+        const UserInfo& user = pair.second;
+
+        // Non-admins can only see themselves
+        if (!isAdmin && user.id != requestingUserId) {
+            continue;
+        }
+
+        // Don't include sensitive information for non-admins
+        UserInfo userInfo = user;
+        if (!isAdmin) {
+            userInfo.passwordHash = "";
+            userInfo.apiKeys.clear();
+        }
+
+        users.push_back(userInfo);
+    }
+
+    return users;
+}
+
+std::pair<bool, std::string> UserManager::createApiKey(const std::string& userId,
+                                                        const std::string& name,
+                                                        const std::vector<std::string>& permissions,
+                                                        int64_t expiresAt,
+                                                        const std::string& createdBy) {
+    try {
+        // Check if user exists
+        bool userExists = false;
+        for (const auto& pair : m_users) {
+            if (pair.second.id == userId) {
+                userExists = true;
+                break;
+            }
+        }
+
+        if (!userExists) {
+            return {false, "User not found"};
+        }
+
+        // Generate API key
+        std::string apiKey = JWTAuth::generateApiKey(32);
+        std::string keyId = generateKeyId();
+        std::string keyHash = hashApiKey(apiKey);
+
+        // Create API key info
+        ApiKeyInfo keyInfo;
+        keyInfo.keyId = keyId;
+        keyInfo.keyHash = keyHash;
+        keyInfo.name = name;
+        keyInfo.userId = userId;
+        keyInfo.permissions = permissions;
+        keyInfo.active = true;
+        keyInfo.createdAt = getCurrentTimestamp();
+        keyInfo.lastUsedAt = 0;
+        keyInfo.expiresAt = expiresAt;
+        keyInfo.createdBy = createdBy;
+
+        m_apiKeys[keyId] = keyInfo;
+        m_apiKeyMap[keyHash] = keyId;
+        saveApiKeyData();
+
+        return {true, apiKey};
+
+    } catch (const std::exception& e) {
+        return {false, "Failed to create API key: " + std::string(e.what())};
+    }
+}
+
+std::pair<bool, std::string> UserManager::revokeApiKey(const std::string& keyId,
+                                                       const std::string& requestingUserId) {
+    try {
+        auto it = m_apiKeys.find(keyId);
+        if (it == m_apiKeys.end()) {
+            return {false, "API key not found"};
+        }
+
+        const ApiKeyInfo& keyInfo = it->second;
+
+        // Check permissions
+        if (keyInfo.userId != requestingUserId) {
+            if (!canManageUser(requestingUserId, keyInfo.userId)) {
+                return {false, "Insufficient permissions to revoke API key"};
+            }
+        }
+
+        // Remove API key
+        m_apiKeyMap.erase(keyInfo.keyHash);
+        m_apiKeys.erase(it);
+        saveApiKeyData();
+
+        return {true, "API key revoked successfully"};
+
+    } catch (const std::exception& e) {
+        return {false, "Failed to revoke API key: " + std::string(e.what())};
+    }
+}
+
+std::vector<ApiKeyInfo> UserManager::listApiKeys(const std::string& userId,
+                                                  const std::string& requestingUserId) {
+    std::vector<ApiKeyInfo> apiKeys;
+
+    // Check if requester is admin or owner
+    UserInfo requester = getUserInfo(requestingUserId);
+    bool isAdmin = (requester.role == roleToString(UserRole::ADMIN));
+    bool isOwner = (requestingUserId == userId);
+
+    for (const auto& pair : m_apiKeys) {
+        const ApiKeyInfo& keyInfo = pair.second;
+
+        // Filter by user ID if specified
+        if (!userId.empty() && keyInfo.userId != userId) {
+            continue;
+        }
+
+        // Check permissions
+        if (!isAdmin && keyInfo.userId != requestingUserId) {
+            continue;
+        }
+
+        // Don't include hash for non-owners
+        ApiKeyInfo keyInfoCopy = keyInfo;
+        if (!isOwner && !isAdmin) {
+            keyInfoCopy.keyHash = "";
+        }
+
+        apiKeys.push_back(keyInfoCopy);
+    }
+
+    return apiKeys;
+}
+
+ApiKeyInfo UserManager::getApiKeyInfo(const std::string& keyId,
+                                       const std::string& requestingUserId) {
+    auto it = m_apiKeys.find(keyId);
+    if (it == m_apiKeys.end()) {
+        return ApiKeyInfo{};
+    }
+
+    const ApiKeyInfo& keyInfo = it->second;
+
+    // Check permissions
+    UserInfo requester = getUserInfo(requestingUserId);
+    bool isAdmin = (requester.role == roleToString(UserRole::ADMIN));
+    bool isOwner = (keyInfo.userId == requestingUserId);
+
+    if (!isAdmin && !isOwner) {
+        return ApiKeyInfo{};
+    }
+
+    // Don't include hash for non-owners
+    ApiKeyInfo keyInfoCopy = keyInfo;
+    if (!isOwner) {
+        keyInfoCopy.keyHash = "";
+    }
+
+    return keyInfoCopy;
+}
+
+void UserManager::updateApiKeyLastUsed(const std::string& keyId) {
+    auto it = m_apiKeys.find(keyId);
+    if (it != m_apiKeys.end()) {
+        it->second.lastUsedAt = getCurrentTimestamp();
+        saveApiKeyData();
+    }
+}
+
+bool UserManager::hasPermission(const std::string& userId, const std::string& permission) {
+    UserInfo user = getUserInfo(userId);
+    if (user.id.empty()) {
+        return false;
+    }
+
+    return JWTAuth::hasPermission(user.permissions, permission);
+}
+
+bool UserManager::hasAnyPermission(const std::string& userId,
+                                   const std::vector<std::string>& permissions) {
+    UserInfo user = getUserInfo(userId);
+    if (user.id.empty()) {
+        return false;
+    }
+
+    return JWTAuth::hasAnyPermission(user.permissions, permissions);
+}
+
+std::string UserManager::roleToString(UserRole role) {
+    switch (role) {
+        case UserRole::GUEST: return "guest";
+        case UserRole::USER: return "user";
+        case UserRole::ADMIN: return "admin";
+        case UserRole::SERVICE: return "service";
+        default: return "unknown";
+    }
+}
+
+UserManager::UserRole UserManager::stringToRole(const std::string& roleStr) {
+    if (roleStr == "guest") return UserRole::GUEST;
+    if (roleStr == "user") return UserRole::USER;
+    if (roleStr == "admin") return UserRole::ADMIN;
+    if (roleStr == "service") return UserRole::SERVICE;
+    return UserRole::USER; // Default
+}
+
+std::vector<std::string> UserManager::getDefaultPermissions(UserRole role) {
+    switch (role) {
+        case UserRole::GUEST:
+            return {Permissions::READ};
+        case UserRole::USER:
+            return {Permissions::READ, Permissions::GENERATE};
+        case UserRole::ADMIN:
+            return {Permissions::READ, Permissions::GENERATE, Permissions::QUEUE_MANAGE,
+                    Permissions::MODEL_MANAGE, Permissions::USER_MANAGE, Permissions::ADMIN};
+        case UserRole::SERVICE:
+            return {Permissions::READ, Permissions::GENERATE, Permissions::QUEUE_MANAGE};
+        default:
+            return {};
+    }
+}
+
+void UserManager::setAuthMethod(AuthMethod method) {
+    m_authMethod = method;
+}
+
+UserManager::AuthMethod UserManager::getAuthMethod() const {
+    return m_authMethod;
+}
+
+void UserManager::setUnixAuthEnabled(bool enable) {
+    m_unixAuthEnabled = enable;
+}
+
+bool UserManager::isUnixAuthEnabled() const {
+    return m_unixAuthEnabled;
+}
+
+std::map<std::string, int> UserManager::getStatistics() {
+    std::map<std::string, int> stats;
+
+    stats["total_users"] = m_users.size();
+    stats["active_users"] = 0;
+    stats["admin_users"] = 0;
+    stats["total_api_keys"] = m_apiKeys.size();
+    stats["active_api_keys"] = 0;
+    stats["expired_api_keys"] = 0;
+
+    int64_t currentTime = getCurrentTimestamp();
+
+    for (const auto& pair : m_users) {
+        const UserInfo& user = pair.second;
+        if (user.active) {
+            stats["active_users"]++;
+        }
+        if (user.role == roleToString(UserRole::ADMIN)) {
+            stats["admin_users"]++;
+        }
+    }
+
+    for (const auto& pair : m_apiKeys) {
+        const ApiKeyInfo& keyInfo = pair.second;
+        if (keyInfo.active) {
+            stats["active_api_keys"]++;
+        }
+        if (keyInfo.expiresAt > 0 && currentTime >= keyInfo.expiresAt) {
+            stats["expired_api_keys"]++;
+        }
+    }
+
+    return stats;
+}
+
+std::string UserManager::hashPassword(const std::string& password) {
+    // Simple SHA256 hash for now (in production, use proper password hashing like bcrypt/argon2)
+    unsigned char hash[SHA256_DIGEST_LENGTH];
+    SHA256((unsigned char*)password.c_str(), password.length(), hash);
+
+    std::stringstream ss;
+    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
+        ss << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i];
+    }
+
+    return "sha256:" + ss.str();
+}
+
+bool UserManager::verifyPassword(const std::string& password, const std::string& storedHash) {
+    // Simple SHA256 verification (in production, use proper password hashing)
+    if (storedHash.substr(0, 7) != "sha256:") {
+        return false;
+    }
+
+    unsigned char hash[SHA256_DIGEST_LENGTH];
+    SHA256((unsigned char*)password.c_str(), password.length(), hash);
+
+    std::stringstream ss;
+    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
+        ss << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i];
+    }
+
+    return "sha256:" + ss.str() == storedHash;
+}
+
+std::string UserManager::hashApiKey(const std::string& apiKey) {
+    // Use SHA256 for API key hashing
+    unsigned char hash[SHA256_DIGEST_LENGTH];
+    SHA256((unsigned char*)apiKey.c_str(), apiKey.length(), hash);
+
+    std::stringstream ss;
+    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
+        ss << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i];
+    }
+
+    return ss.str();
+}
+
+std::string UserManager::generateUserId() {
+    return "user_" + std::to_string(getCurrentTimestamp()) + "_" +
+           std::to_string(rand() % 10000);
+}
+
+std::string UserManager::generateKeyId() {
+    return "key_" + std::to_string(getCurrentTimestamp()) + "_" +
+           std::to_string(rand() % 10000);
+}
+
+bool UserManager::saveUserData() {
+    try {
+        json usersJson = json::object();
+
+        for (const auto& pair : m_users) {
+            const UserInfo& user = pair.second;
+            json userJson = {
+                {"id", user.id},
+                {"username", user.username},
+                {"email", user.email},
+                {"password_hash", user.passwordHash},
+                {"role", user.role},
+                {"permissions", user.permissions},
+                {"api_keys", user.apiKeys},
+                {"active", user.active},
+                {"created_at", user.createdAt},
+                {"last_login_at", user.lastLoginAt},
+                {"password_changed_at", user.passwordChangedAt},
+                {"created_by", user.createdBy}
+            };
+            usersJson[user.username] = userJson;
+        }
+
+        std::string filename = m_dataDir + "/users.json";
+        std::ofstream file(filename);
+        if (!file.is_open()) {
+            return false;
+        }
+
+        file << usersJson.dump(2);
+        file.close();
+
+        return true;
+
+    } catch (const std::exception& e) {
+        return false;
+    }
+}
+
+bool UserManager::loadUserData() {
+    try {
+        std::string filename = m_dataDir + "/users.json";
+        std::ifstream file(filename);
+        if (!file.is_open()) {
+            return false; // File doesn't exist is OK for first run
+        }
+
+        json usersJson;
+        file >> usersJson;
+        file.close();
+
+        m_users.clear();
+
+        for (auto& item : usersJson.items()) {
+            const std::string& username = item.key();
+            json& userJson = item.value();
+
+            UserInfo user;
+            user.id = userJson.value("id", "");
+            user.username = userJson.value("username", username);
+            user.email = userJson.value("email", "");
+            user.passwordHash = userJson.value("password_hash", "");
+            user.role = userJson.value("role", "user");
+            user.permissions = userJson.value("permissions", std::vector<std::string>{});
+            user.apiKeys = userJson.value("api_keys", std::vector<std::string>{});
+            user.active = userJson.value("active", true);
+            user.createdAt = userJson.value("created_at", 0);
+            user.lastLoginAt = userJson.value("last_login_at", 0);
+            user.passwordChangedAt = userJson.value("password_changed_at", 0);
+            user.createdBy = userJson.value("created_by", "system");
+
+            m_users[username] = user;
+        }
+
+        return true;
+
+    } catch (const std::exception& e) {
+        return false;
+    }
+}
+
+bool UserManager::saveApiKeyData() {
+    try {
+        json apiKeysJson = json::object();
+
+        for (const auto& pair : m_apiKeys) {
+            const ApiKeyInfo& keyInfo = pair.second;
+            json keyJson = {
+                {"key_id", keyInfo.keyId},
+                {"key_hash", keyInfo.keyHash},
+                {"name", keyInfo.name},
+                {"user_id", keyInfo.userId},
+                {"permissions", keyInfo.permissions},
+                {"active", keyInfo.active},
+                {"created_at", keyInfo.createdAt},
+                {"last_used_at", keyInfo.lastUsedAt},
+                {"expires_at", keyInfo.expiresAt},
+                {"created_by", keyInfo.createdBy}
+            };
+            apiKeysJson[keyInfo.keyId] = keyJson;
+        }
+
+        std::string filename = m_dataDir + "/api_keys.json";
+        std::ofstream file(filename);
+        if (!file.is_open()) {
+            return false;
+        }
+
+        file << apiKeysJson.dump(2);
+        file.close();
+
+        return true;
+
+    } catch (const std::exception& e) {
+        return false;
+    }
+}
+
+bool UserManager::loadApiKeyData() {
+    try {
+        std::string filename = m_dataDir + "/api_keys.json";
+        std::ifstream file(filename);
+        if (!file.is_open()) {
+            return false; // File doesn't exist is OK for first run
+        }
+
+        json apiKeysJson;
+        file >> apiKeysJson;
+        file.close();
+
+        m_apiKeys.clear();
+        m_apiKeyMap.clear();
+
+        for (auto& item : apiKeysJson.items()) {
+            const std::string& keyId = item.key();
+            json& keyJson = item.value();
+
+            ApiKeyInfo keyInfo;
+            keyInfo.keyId = keyJson.value("key_id", keyId);
+            keyInfo.keyHash = keyJson.value("key_hash", "");
+            keyInfo.name = keyJson.value("name", "");
+            keyInfo.userId = keyJson.value("user_id", "");
+            keyInfo.permissions = keyJson.value("permissions", std::vector<std::string>{});
+            keyInfo.active = keyJson.value("active", true);
+            keyInfo.createdAt = keyJson.value("created_at", 0);
+            keyInfo.lastUsedAt = keyJson.value("last_used_at", 0);
+            keyInfo.expiresAt = keyJson.value("expires_at", 0);
+            keyInfo.createdBy = keyJson.value("created_by", "system");
+
+            m_apiKeys[keyId] = keyInfo;
+            m_apiKeyMap[keyInfo.keyHash] = keyId;
+        }
+
+        return true;
+
+    } catch (const std::exception& e) {
+        return false;
+    }
+}
+
+int64_t UserManager::getCurrentTimestamp() {
+    return std::chrono::duration_cast<std::chrono::seconds>(
+        std::chrono::system_clock::now().time_since_epoch()).count();
+}
+
+bool UserManager::validateUsername(const std::string& username) {
+    if (username.length() < 3 || username.length() > 32) {
+        return false;
+    }
+
+    // Username should contain only alphanumeric characters, underscores, and hyphens
+    std::regex pattern("^[a-zA-Z0-9_-]+$");
+    return std::regex_match(username, pattern);
+}
+
+bool UserManager::validatePassword(const std::string& password) {
+    if (password.length() < 8 || password.length() > 128) {
+        return false;
+    }
+
+    // Password should contain at least one letter and one digit
+    bool hasLetter = false;
+    bool hasDigit = false;
+
+    for (char c : password) {
+        if (std::isalpha(c)) hasLetter = true;
+        if (std::isdigit(c)) hasDigit = true;
+    }
+
+    return hasLetter && hasDigit;
+}
+
+bool UserManager::validateEmail(const std::string& email) {
+    if (email.length() < 5 || email.length() > 254) {
+        return false;
+    }
+
+    // Basic email validation
+    std::regex pattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
+    return std::regex_match(email, pattern);
+}
+
+bool UserManager::canManageUser(const std::string& requestingUserId, const std::string& targetUserId) {
+    // Users can always manage themselves
+    if (requestingUserId == targetUserId) {
+        return true;
+    }
+
+    // Check if requester is admin
+    UserInfo requester = getUserInfo(requestingUserId);
+    return requester.role == roleToString(UserRole::ADMIN);
+}

+ 5 - 2
webui/app/layout.tsx

@@ -3,6 +3,7 @@ import { Inter } from "next/font/google";
 import "./globals.css";
 import { ThemeProvider } from "@/components/theme-provider";
 import { VersionChecker } from "@/components/version-checker";
+import { AuthProvider } from "@/lib/auth-context";
 
 const inter = Inter({
   subsets: ["latin"],
@@ -33,8 +34,10 @@ export default function RootLayout({
           enableSystem
           disableTransitionOnChange
         >
-          <VersionChecker />
-          {children}
+          <AuthProvider>
+            <VersionChecker />
+            {children}
+          </AuthProvider>
         </ThemeProvider>
       </body>
     </html>

+ 30 - 3
webui/app/page.tsx

@@ -7,7 +7,9 @@ import { AppLayout } from '@/components/layout';
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
 import { Button } from '@/components/ui/button';
 import { apiClient } from '@/lib/api';
-import { ImagePlus, Image, Sparkles, Settings, Activity, ArrowRight, CheckCircle2, XCircle } from 'lucide-react';
+import { ProtectedRoute } from '@/components/auth/protected-route';
+import { useAuth } from '@/lib/auth-context';
+import { ImagePlus, Image, Sparkles, Settings, Activity, ArrowRight, CheckCircle2, XCircle, User, LogOut } from 'lucide-react';
 
 const features = [
   {
@@ -48,6 +50,7 @@ const features = [
 ];
 
 export default function HomePage() {
+  const { user, logout } = useAuth();
   const [health, setHealth] = useState<'checking' | 'healthy' | 'error'>('checking');
   const [systemInfo, setSystemInfo] = useState<any>(null);
 
@@ -75,8 +78,31 @@ export default function HomePage() {
   };
 
   return (
-    <AppLayout>
-      <Header title="Stable Diffusion REST" description="Modern web interface for AI image generation" />
+    <ProtectedRoute>
+      <AppLayout>
+        <Header
+          title="Stable Diffusion REST"
+          description="Modern web interface for AI image generation"
+          actions={
+            <div className="flex items-center gap-4">
+              <div className="flex items-center gap-2 text-sm">
+                <User className="h-4 w-4" />
+                <span>{user?.username}</span>
+                <span className="text-muted-foreground">({user?.role})</span>
+              </div>
+              <Button variant="outline" size="sm" onClick={logout}>
+                <LogOut className="h-4 w-4 mr-2" />
+                Sign Out
+              </Button>
+              <Link href="/settings">
+                <Button variant="outline" size="sm">
+                  <Settings className="h-4 w-4 mr-2" />
+                  Settings
+                </Button>
+              </Link>
+            </div>
+          }
+        />
       <div className="container mx-auto p-6">
         <div className="space-y-8">
           {/* Status Banner */}
@@ -223,5 +249,6 @@ export default function HomePage() {
         </div>
       </div>
     </AppLayout>
+    </ProtectedRoute>
   );
 }

+ 191 - 0
webui/app/settings/page.tsx

@@ -0,0 +1,191 @@
+"use client"
+
+import React, { useState } from 'react'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { ProtectedRoute } from '@/components/auth/protected-route'
+import { ApiKeyManager } from '@/components/auth/api-key-manager'
+import { UserManagement } from '@/components/auth/user-management'
+import { useAuth } from '@/lib/auth-context'
+import { authApi } from '@/lib/api'
+
+export default function SettingsPage() {
+  const { user, logout } = useAuth()
+  const [activeTab, setActiveTab] = useState('profile')
+  const [error, setError] = useState<string | null>(null)
+  const [success, setSuccess] = useState<string | null>(null)
+  const [passwordData, setPasswordData] = useState({
+    current_password: '',
+    new_password: '',
+    confirm_password: ''
+  })
+
+  const handlePasswordChange = async (e: React.FormEvent) => {
+    e.preventDefault()
+
+    if (passwordData.new_password !== passwordData.confirm_password) {
+      setError('New passwords do not match')
+      return
+    }
+
+    if (passwordData.new_password.length < 8) {
+      setError('Password must be at least 8 characters long')
+      return
+    }
+
+    try {
+      await authApi.changePassword(passwordData.current_password, passwordData.new_password)
+      setSuccess('Password changed successfully')
+      setPasswordData({
+        current_password: '',
+        new_password: '',
+        confirm_password: ''
+      })
+      setError(null)
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Failed to change password')
+      setSuccess(null)
+    }
+  }
+
+  return (
+    <ProtectedRoute>
+      <div className="container mx-auto py-6 space-y-6">
+        <div>
+          <h1 className="text-3xl font-bold">Settings</h1>
+          <p className="text-muted-foreground">Manage your account and application settings</p>
+        </div>
+
+        {error && (
+          <Alert variant="destructive">
+            <AlertDescription>{error}</AlertDescription>
+          </Alert>
+        )}
+
+        {success && (
+          <Alert>
+            <AlertDescription>{success}</AlertDescription>
+          </Alert>
+        )}
+
+        <Tabs value={activeTab} onValueChange={setActiveTab}>
+          <TabsList className="grid w-full grid-cols-3">
+            <TabsTrigger value="profile">Profile</TabsTrigger>
+            <TabsTrigger value="api-keys">API Keys</TabsTrigger>
+            {user?.role === 'admin' && (
+              <TabsTrigger value="users">User Management</TabsTrigger>
+            )}
+          </TabsList>
+
+          <TabsContent value="profile" className="space-y-6">
+            <Card>
+              <CardHeader>
+                <CardTitle>Profile Information</CardTitle>
+                <CardDescription>
+                  View and manage your account details
+                </CardDescription>
+              </CardHeader>
+              <CardContent className="space-y-4">
+                <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                  <div>
+                    <Label>Username</Label>
+                    <Input value={user?.username || ''} disabled />
+                  </div>
+                  <div>
+                    <Label>Role</Label>
+                    <Input value={user?.role || ''} disabled />
+                  </div>
+                  {user?.email && (
+                    <div>
+                      <Label>Email</Label>
+                      <Input value={user.email} disabled />
+                    </div>
+                  )}
+                  <div>
+                    <Label>Account Created</Label>
+                    <Input
+                      value={user?.createdAt ? new Date(user.createdAt).toLocaleDateString() : ''}
+                      disabled
+                    />
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+
+            <Card>
+              <CardHeader>
+                <CardTitle>Change Password</CardTitle>
+                <CardDescription>
+                  Update your account password
+                </CardDescription>
+              </CardHeader>
+              <CardContent>
+                <form onSubmit={handlePasswordChange} className="space-y-4">
+                  <div>
+                    <Label htmlFor="current_password">Current Password</Label>
+                    <Input
+                      id="current_password"
+                      type="password"
+                      value={passwordData.current_password}
+                      onChange={(e) => setPasswordData(prev => ({ ...prev, current_password: e.target.value }))}
+                      required
+                    />
+                  </div>
+                  <div>
+                    <Label htmlFor="new_password">New Password</Label>
+                    <Input
+                      id="new_password"
+                      type="password"
+                      value={passwordData.new_password}
+                      onChange={(e) => setPasswordData(prev => ({ ...prev, new_password: e.target.value }))}
+                      required
+                    />
+                  </div>
+                  <div>
+                    <Label htmlFor="confirm_password">Confirm New Password</Label>
+                    <Input
+                      id="confirm_password"
+                      type="password"
+                      value={passwordData.confirm_password}
+                      onChange={(e) => setPasswordData(prev => ({ ...prev, confirm_password: e.target.value }))}
+                      required
+                    />
+                  </div>
+                  <Button type="submit">Change Password</Button>
+                </form>
+              </CardContent>
+            </Card>
+
+            <Card>
+              <CardHeader>
+                <CardTitle>Account Actions</CardTitle>
+                <CardDescription>
+                  Manage your session
+                </CardDescription>
+              </CardHeader>
+              <CardContent>
+                <Button variant="destructive" onClick={logout}>
+                  Sign Out
+                </Button>
+              </CardContent>
+            </Card>
+          </TabsContent>
+
+          <TabsContent value="api-keys">
+            <ApiKeyManager />
+          </TabsContent>
+
+          {user?.role === 'admin' && (
+            <TabsContent value="users">
+              <UserManagement />
+            </TabsContent>
+          )}
+        </Tabs>
+      </div>
+    </ProtectedRoute>
+  )
+}

+ 251 - 0
webui/components/auth/api-key-manager.tsx

@@ -0,0 +1,251 @@
+"use client"
+
+import React, { useState, useEffect } from 'react'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+import { Badge } from '@/components/ui/badge'
+import { authApi } from '@/lib/api'
+import { useAuth } from '@/lib/auth-context'
+
+interface ApiKey {
+  id: string
+  name: string
+  key: string
+  scopes: string[]
+  created_at: string
+  last_used?: string
+  expires_at?: string
+  active: boolean
+}
+
+export function ApiKeyManager() {
+  const { user } = useAuth()
+  const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
+  const [loading, setLoading] = useState(true)
+  const [error, setError] = useState<string | null>(null)
+  const [showCreateForm, setShowCreateForm] = useState(false)
+  const [newKeyName, setNewKeyName] = useState('')
+  const [newKeyScopes, setNewKeyScopes] = useState<string[]>([])
+  const [createdKey, setCreatedKey] = useState<ApiKey | null>(null)
+
+  useEffect(() => {
+    loadApiKeys()
+  }, [])
+
+  const loadApiKeys = async () => {
+    try {
+      setLoading(true)
+      const data = await authApi.getApiKeys()
+      setApiKeys(data.keys || [])
+      setError(null)
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Failed to load API keys')
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  const handleCreateKey = async (e: React.FormEvent) => {
+    e.preventDefault()
+
+    if (!newKeyName.trim()) {
+      setError('Key name is required')
+      return
+    }
+
+    try {
+      const data = await authApi.createApiKey(newKeyName.trim(), newKeyScopes)
+      setCreatedKey(data.key)
+      setApiKeys(prev => [data.key, ...prev])
+      setNewKeyName('')
+      setNewKeyScopes([])
+      setShowCreateForm(false)
+      setError(null)
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Failed to create API key')
+    }
+  }
+
+  const handleDeleteKey = async (keyId: string) => {
+    if (!confirm('Are you sure you want to delete this API key? This action cannot be undone.')) {
+      return
+    }
+
+    try {
+      await authApi.deleteApiKey(keyId)
+      setApiKeys(prev => prev.filter(key => key.id !== keyId))
+      setError(null)
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Failed to delete API key')
+    }
+  }
+
+  const copyToClipboard = (text: string) => {
+    navigator.clipboard.writeText(text).then(() => {
+      // You could add a toast notification here
+    })
+  }
+
+  const formatDate = (dateString: string) => {
+    return new Date(dateString).toLocaleDateString() + ' ' + new Date(dateString).toLocaleTimeString()
+  }
+
+  if (loading) {
+    return <div className="p-4">Loading API keys...</div>
+  }
+
+  return (
+    <div className="space-y-6">
+      {error && (
+        <Alert variant="destructive">
+          <AlertDescription>{error}</AlertDescription>
+        </Alert>
+      )}
+
+      {createdKey && (
+        <Alert>
+          <AlertDescription>
+            <div className="space-y-2">
+              <p><strong>API Key Created Successfully!</strong></p>
+              <p className="text-sm text-muted-foreground">
+                Please copy this key now. You won't be able to see it again.
+              </p>
+              <div className="flex items-center space-x-2">
+                <code className="bg-muted px-2 py-1 rounded text-sm font-mono">
+                  {createdKey.key}
+                </code>
+                <Button
+                  size="sm"
+                  variant="outline"
+                  onClick={() => copyToClipboard(createdKey.key)}
+                >
+                  Copy
+                </Button>
+              </div>
+            </div>
+          </AlertDescription>
+        </Alert>
+      )}
+
+      <Card>
+        <CardHeader>
+          <div className="flex justify-between items-center">
+            <div>
+              <CardTitle>API Keys</CardTitle>
+              <CardDescription>
+                Manage API keys for programmatic access to the Stable Diffusion API
+              </CardDescription>
+            </div>
+            <Button onClick={() => setShowCreateForm(!showCreateForm)}>
+              {showCreateForm ? 'Cancel' : 'Create New Key'}
+            </Button>
+          </div>
+        </CardHeader>
+        <CardContent>
+          {showCreateForm && (
+            <form onSubmit={handleCreateKey} className="space-y-4 mb-6 p-4 border rounded-lg">
+              <div>
+                <Label htmlFor="keyName">Key Name</Label>
+                <Input
+                  id="keyName"
+                  value={newKeyName}
+                  onChange={(e) => setNewKeyName(e.target.value)}
+                  placeholder="e.g., My Application Key"
+                  required
+                />
+              </div>
+              <div>
+                <Label>Scopes (optional)</Label>
+                <div className="space-y-2 mt-2">
+                  {['generate', 'models', 'queue', 'system'].map((scope) => (
+                    <label key={scope} className="flex items-center space-x-2">
+                      <input
+                        type="checkbox"
+                        checked={newKeyScopes.includes(scope)}
+                        onChange={(e) => {
+                          if (e.target.checked) {
+                            setNewKeyScopes(prev => [...prev, scope])
+                          } else {
+                            setNewKeyScopes(prev => prev.filter(s => s !== scope))
+                          }
+                        }}
+                      />
+                      <span className="text-sm">{scope}</span>
+                    </label>
+                  ))}
+                </div>
+              </div>
+              <div className="flex space-x-2">
+                <Button type="submit">Create Key</Button>
+                <Button type="button" variant="outline" onClick={() => setShowCreateForm(false)}>
+                  Cancel
+                </Button>
+              </div>
+            </form>
+          )}
+
+          {apiKeys.length === 0 ? (
+            <div className="text-center py-8 text-muted-foreground">
+              No API keys found. Create your first key to get started.
+            </div>
+          ) : (
+            <div className="space-y-4">
+              {apiKeys.map((apiKey) => (
+                <div key={apiKey.id} className="border rounded-lg p-4">
+                  <div className="flex justify-between items-start">
+                    <div className="space-y-2">
+                      <div className="flex items-center space-x-2">
+                        <h3 className="font-medium">{apiKey.name}</h3>
+                        <Badge variant={apiKey.active ? "default" : "secondary"}>
+                          {apiKey.active ? 'Active' : 'Inactive'}
+                        </Badge>
+                      </div>
+                      <div className="text-sm text-muted-foreground">
+                        <p>Key: {apiKey.key.substring(0, 8)}...</p>
+                        <p>Created: {formatDate(apiKey.created_at)}</p>
+                        {apiKey.last_used && (
+                          <p>Last used: {formatDate(apiKey.last_used)}</p>
+                        )}
+                        {apiKey.expires_at && (
+                          <p>Expires: {formatDate(apiKey.expires_at)}</p>
+                        )}
+                      </div>
+                      {apiKey.scopes.length > 0 && (
+                        <div className="flex flex-wrap gap-1">
+                          {apiKey.scopes.map((scope) => (
+                            <Badge key={scope} variant="outline" className="text-xs">
+                              {scope}
+                            </Badge>
+                          ))}
+                        </div>
+                      )}
+                    </div>
+                    <div className="flex space-x-2">
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        onClick={() => copyToClipboard(apiKey.key)}
+                      >
+                        Copy
+                      </Button>
+                      <Button
+                        size="sm"
+                        variant="destructive"
+                        onClick={() => handleDeleteKey(apiKey.id)}
+                      >
+                        Delete
+                      </Button>
+                    </div>
+                  </div>
+                </div>
+              ))}
+            </div>
+          )}
+        </CardContent>
+      </Card>
+    </div>
+  )
+}

+ 88 - 0
webui/components/auth/login-form.tsx

@@ -0,0 +1,88 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { useAuth } from "@/lib/auth-context";
+import { Loader2 } from "lucide-react";
+
+interface LoginFormProps {
+  onSuccess?: () => void;
+  onError?: (error: string) => void;
+}
+
+export function LoginForm({ onSuccess, onError }: LoginFormProps) {
+  const [username, setUsername] = useState("");
+  const [password, setPassword] = useState("");
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState("");
+  const { login } = useAuth();
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setError("");
+    setIsLoading(true);
+
+    try {
+      await login(username, password);
+      onSuccess?.();
+    } catch (err) {
+      const errorMessage = err instanceof Error ? err.message : "An unexpected error occurred";
+      setError(errorMessage);
+      onError?.(errorMessage);
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  return (
+    <Card className="w-full max-w-md mx-auto">
+      <CardHeader>
+        <CardTitle>Login</CardTitle>
+        <CardDescription>
+          Enter your credentials to access the Stable Diffusion REST API
+        </CardDescription>
+      </CardHeader>
+      <CardContent>
+        <form onSubmit={handleSubmit} className="space-y-4">
+          <div className="space-y-2">
+            <Label htmlFor="username">Username</Label>
+            <Input
+              id="username"
+              type="text"
+              placeholder="Enter your username"
+              value={username}
+              onChange={(e) => setUsername(e.target.value)}
+              required
+              disabled={isLoading}
+            />
+          </div>
+          <div className="space-y-2">
+            <Label htmlFor="password">Password</Label>
+            <Input
+              id="password"
+              type="password"
+              placeholder="Enter your password"
+              value={password}
+              onChange={(e) => setPassword(e.target.value)}
+              required
+              disabled={isLoading}
+            />
+          </div>
+          {error && (
+            <Alert variant="destructive">
+              <AlertDescription>{error}</AlertDescription>
+            </Alert>
+          )}
+          <Button type="submit" className="w-full" disabled={isLoading}>
+            {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+            {isLoading ? "Signing in..." : "Sign in"}
+          </Button>
+        </form>
+      </CardContent>
+    </Card>
+  );
+}

+ 73 - 0
webui/components/auth/protected-route.tsx

@@ -0,0 +1,73 @@
+"use client"
+
+import React, { useEffect } from 'react'
+import { useAuth } from '@/lib/auth-context'
+import { LoginForm } from './login-form'
+
+interface ProtectedRouteProps {
+  children: React.ReactNode
+  requiredRole?: 'admin' | 'user'
+  fallback?: React.ReactNode
+}
+
+export function ProtectedRoute({ children, requiredRole, fallback }: ProtectedRouteProps) {
+  const { isAuthenticated, isLoading, user, error } = useAuth()
+
+  if (isLoading) {
+    return (
+      <div className="flex items-center justify-center min-h-screen">
+        <div className="text-center">
+          <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
+          <p className="text-muted-foreground">Loading...</p>
+        </div>
+      </div>
+    )
+  }
+
+  if (!isAuthenticated) {
+    if (fallback) {
+      return <>{fallback}</>
+    }
+    return (
+      <div className="flex items-center justify-center min-h-screen bg-background">
+        <div className="w-full max-w-md p-6">
+          <div className="text-center mb-8">
+            <h1 className="text-3xl font-bold">Stable Diffusion</h1>
+            <p className="text-muted-foreground mt-2">Please sign in to continue</p>
+          </div>
+          <LoginForm />
+        </div>
+      </div>
+    )
+  }
+
+  // Check role requirements
+  if (requiredRole && user?.role !== requiredRole) {
+    return (
+      <div className="flex items-center justify-center min-h-screen">
+        <div className="text-center">
+          <h1 className="text-2xl font-bold text-destructive mb-4">Access Denied</h1>
+          <p className="text-muted-foreground">
+            You don't have permission to access this page.
+          </p>
+        </div>
+      </div>
+    )
+  }
+
+  return <>{children}</>
+}
+
+// Higher-order component for protecting routes
+export function withAuth<P extends object>(
+  Component: React.ComponentType<P>,
+  options?: { requiredRole?: 'admin' | 'user' }
+) {
+  return function AuthenticatedComponent(props: P) {
+    return (
+      <ProtectedRoute requiredRole={options?.requiredRole}>
+        <Component {...props} />
+      </ProtectedRoute>
+    )
+  }
+}

+ 309 - 0
webui/components/auth/user-management.tsx

@@ -0,0 +1,309 @@
+"use client"
+
+import React, { useState, useEffect } from 'react'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+import { Badge } from '@/components/ui/badge'
+import { authApi } from '@/lib/api'
+import { useAuth } from '@/lib/auth-context'
+
+interface User {
+  id: string
+  username: string
+  email?: string
+  role: 'admin' | 'user'
+  active: boolean
+  created_at: string
+  last_login?: string
+}
+
+export function UserManagement() {
+  const { user: currentUser } = useAuth()
+  const [users, setUsers] = useState<User[]>([])
+  const [loading, setLoading] = useState(true)
+  const [error, setError] = useState<string | null>(null)
+  const [showCreateForm, setShowCreateForm] = useState(false)
+  const [editingUser, setEditingUser] = useState<User | null>(null)
+  const [formData, setFormData] = useState({
+    username: '',
+    email: '',
+    password: '',
+    role: 'user' as 'admin' | 'user',
+    active: true
+  })
+
+  useEffect(() => {
+    loadUsers()
+  }, [])
+
+  const loadUsers = async () => {
+    try {
+      setLoading(true)
+      const data = await authApi.getUsers()
+      setUsers(data.users || [])
+      setError(null)
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Failed to load users')
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  const handleCreateUser = async (e: React.FormEvent) => {
+    e.preventDefault()
+
+    if (!formData.username.trim() || !formData.password.trim()) {
+      setError('Username and password are required')
+      return
+    }
+
+    try {
+      await authApi.createUser({
+        username: formData.username.trim(),
+        email: formData.email.trim() || undefined,
+        password: formData.password,
+        role: formData.role
+      })
+
+      await loadUsers()
+      setFormData({
+        username: '',
+        email: '',
+        password: '',
+        role: 'user',
+        active: true
+      })
+      setShowCreateForm(false)
+      setError(null)
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Failed to create user')
+    }
+  }
+
+  const handleUpdateUser = async (e: React.FormEvent) => {
+    e.preventDefault()
+
+    if (!editingUser) return
+
+    try {
+      await authApi.updateUser(editingUser.id, {
+        email: formData.email.trim() || undefined,
+        role: formData.role,
+        active: formData.active
+      })
+
+      await loadUsers()
+      setEditingUser(null)
+      setFormData({
+        username: '',
+        email: '',
+        password: '',
+        role: 'user',
+        active: true
+      })
+      setError(null)
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Failed to update user')
+    }
+  }
+
+  const handleDeleteUser = async (userId: string, username: string) => {
+    if (!confirm(`Are you sure you want to delete user "${username}"? This action cannot be undone.`)) {
+      return
+    }
+
+    try {
+      await authApi.deleteUser(userId)
+      setUsers(prev => prev.filter(user => user.id !== userId))
+      setError(null)
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Failed to delete user')
+    }
+  }
+
+  const startEditUser = (user: User) => {
+    setEditingUser(user)
+    setFormData({
+      username: user.username,
+      email: user.email || '',
+      password: '',
+      role: user.role,
+      active: user.active
+    })
+  }
+
+  const cancelEdit = () => {
+    setEditingUser(null)
+    setFormData({
+      username: '',
+      email: '',
+      password: '',
+      role: 'user',
+      active: true
+    })
+  }
+
+  const formatDate = (dateString: string) => {
+    return new Date(dateString).toLocaleDateString() + ' ' + new Date(dateString).toLocaleTimeString()
+  }
+
+  if (loading) {
+    return <div className="p-4">Loading users...</div>
+  }
+
+  return (
+    <div className="space-y-6">
+      {error && (
+        <Alert variant="destructive">
+          <AlertDescription>{error}</AlertDescription>
+        </Alert>
+      )}
+
+      <Card>
+        <CardHeader>
+          <div className="flex justify-between items-center">
+            <div>
+              <CardTitle>User Management</CardTitle>
+              <CardDescription>
+                Manage user accounts and permissions
+              </CardDescription>
+            </div>
+            <Button onClick={() => setShowCreateForm(!showCreateForm)}>
+              {showCreateForm ? 'Cancel' : 'Create User'}
+            </Button>
+          </div>
+        </CardHeader>
+        <CardContent>
+          {(showCreateForm || editingUser) && (
+            <form onSubmit={editingUser ? handleUpdateUser : handleCreateUser} className="space-y-4 mb-6 p-4 border rounded-lg">
+              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                <div>
+                  <Label htmlFor="username">Username</Label>
+                  <Input
+                    id="username"
+                    value={formData.username}
+                    onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value }))}
+                    placeholder="Username"
+                    required
+                    disabled={!!editingUser}
+                  />
+                </div>
+                <div>
+                  <Label htmlFor="email">Email</Label>
+                  <Input
+                    id="email"
+                    type="email"
+                    value={formData.email}
+                    onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
+                    placeholder="Email (optional)"
+                  />
+                </div>
+                {!editingUser && (
+                  <div>
+                    <Label htmlFor="password">Password</Label>
+                    <Input
+                      id="password"
+                      type="password"
+                      value={formData.password}
+                      onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
+                      placeholder="Password"
+                      required
+                    />
+                  </div>
+                )}
+                <div>
+                  <Label htmlFor="role">Role</Label>
+                  <select
+                    id="role"
+                    value={formData.role}
+                    onChange={(e) => setFormData(prev => ({ ...prev, role: e.target.value as 'admin' | 'user' }))}
+                    className="w-full p-2 border rounded-md"
+                  >
+                    <option value="user">User</option>
+                    <option value="admin">Admin</option>
+                  </select>
+                </div>
+              </div>
+              {editingUser && (
+                <div className="flex items-center space-x-2">
+                  <input
+                    type="checkbox"
+                    id="active"
+                    checked={formData.active}
+                    onChange={(e) => setFormData(prev => ({ ...prev, active: e.target.checked }))}
+                  />
+                  <Label htmlFor="active">Active</Label>
+                </div>
+              )}
+              <div className="flex space-x-2">
+                <Button type="submit">
+                  {editingUser ? 'Update User' : 'Create User'}
+                </Button>
+                <Button type="button" variant="outline" onClick={editingUser ? cancelEdit : () => setShowCreateForm(false)}>
+                  Cancel
+                </Button>
+              </div>
+            </form>
+          )}
+
+          {users.length === 0 ? (
+            <div className="text-center py-8 text-muted-foreground">
+              No users found. Create your first user to get started.
+            </div>
+          ) : (
+            <div className="space-y-4">
+              {users.map((user) => (
+                <div key={user.id} className="border rounded-lg p-4">
+                  <div className="flex justify-between items-start">
+                    <div className="space-y-2">
+                      <div className="flex items-center space-x-2">
+                        <h3 className="font-medium">{user.username}</h3>
+                        <Badge variant={user.role === 'admin' ? "default" : "secondary"}>
+                          {user.role}
+                        </Badge>
+                        <Badge variant={user.active ? "default" : "destructive"}>
+                          {user.active ? 'Active' : 'Inactive'}
+                        </Badge>
+                        {user.id === currentUser?.id && (
+                          <Badge variant="outline">You</Badge>
+                        )}
+                      </div>
+                      <div className="text-sm text-muted-foreground">
+                        {user.email && <p>Email: {user.email}</p>}
+                        <p>Created: {formatDate(user.created_at)}</p>
+                        {user.last_login && (
+                          <p>Last login: {formatDate(user.last_login)}</p>
+                        )}
+                      </div>
+                    </div>
+                    <div className="flex space-x-2">
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        onClick={() => startEditUser(user)}
+                      >
+                        Edit
+                      </Button>
+                      {user.id !== currentUser?.id && (
+                        <Button
+                          size="sm"
+                          variant="destructive"
+                          onClick={() => handleDeleteUser(user.id, user.username)}
+                        >
+                          Delete
+                        </Button>
+                      )}
+                    </div>
+                  </div>
+                </div>
+              ))}
+            </div>
+          )}
+        </CardContent>
+      </Card>
+    </div>
+  )
+}

+ 533 - 0
webui/components/enhanced-queue-list.tsx

@@ -0,0 +1,533 @@
+'use client';
+
+import { useState, useMemo } from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Progress } from '@/components/ui/progress';
+import { type QueueStatus, type JobInfo, type GenerationRequest } from '@/lib/api';
+import {
+  RefreshCw,
+  Trash2,
+  CheckCircle2,
+  XCircle,
+  Loader2,
+  Clock,
+  Image,
+  Activity,
+  AlertCircle,
+  Copy,
+  Eye,
+  Calendar,
+  Settings,
+  FileText,
+  Zap,
+  ArrowRight
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+interface EnhancedQueueListProps {
+  queueStatus: QueueStatus | null;
+  loading: boolean;
+  onRefresh: () => void;
+  onCancelJob: (jobId: string) => void;
+  onClearQueue: () => void;
+  actionLoading: boolean;
+  onCopyParameters?: (job: JobInfo) => void;
+}
+
+type ViewMode = 'compact' | 'detailed';
+
+export function EnhancedQueueList({
+  queueStatus,
+  loading,
+  onRefresh,
+  onCancelJob,
+  onClearQueue,
+  actionLoading,
+  onCopyParameters
+}: EnhancedQueueListProps) {
+  const [viewMode, setViewMode] = useState<ViewMode>('detailed');
+  const [selectedJob, setSelectedJob] = useState<string | null>(null);
+
+  // Calculate queue statistics
+  const queueStats = useMemo(() => {
+    if (!queueStatus?.jobs) return { total: 0, active: 0, queued: 0, completed: 0, failed: 0 };
+
+    return {
+      total: queueStatus.jobs.length,
+      active: queueStatus.jobs.filter(job => job.status === 'processing').length,
+      queued: queueStatus.jobs.filter(job => job.status === 'queued').length,
+      completed: queueStatus.jobs.filter(job => job.status === 'completed').length,
+      failed: queueStatus.jobs.filter(job => job.status === 'failed').length,
+    };
+  }, [queueStatus]);
+
+  // Sort jobs by status and creation time
+  const sortedJobs = useMemo(() => {
+    if (!queueStatus?.jobs) return [];
+
+    const statusPriority = { processing: 0, queued: 1, pending: 2, completed: 3, failed: 4, cancelled: 5 };
+
+    return [...queueStatus.jobs].sort((a, b) => {
+      const statusDiff = statusPriority[a.status as keyof typeof statusPriority] -
+                        statusPriority[b.status as keyof typeof statusPriority];
+      if (statusDiff !== 0) return statusDiff;
+
+      // If same status, sort by creation time (newest first)
+      const timeA = new Date(a.created_at || 0).getTime();
+      const timeB = new Date(b.created_at || 0).getTime();
+      return timeB - timeA;
+    });
+  }, [queueStatus]);
+
+  const getStatusIcon = (status: string) => {
+    switch (status) {
+      case 'completed':
+        return <CheckCircle2 className="h-4 w-4 text-green-500" />;
+      case 'failed':
+        return <XCircle className="h-4 w-4 text-red-500" />;
+      case 'processing':
+        return <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />;
+      case 'queued':
+        return <Clock className="h-4 w-4 text-yellow-500" />;
+      case 'cancelled':
+        return <XCircle className="h-4 w-4 text-gray-500" />;
+      default:
+        return <AlertCircle className="h-4 w-4 text-gray-500" />;
+    }
+  };
+
+  const getStatusColor = (status: string) => {
+    switch (status) {
+      case 'completed':
+        return 'text-green-600 dark:text-green-400';
+      case 'failed':
+        return 'text-red-600 dark:text-red-400';
+      case 'processing':
+        return 'text-blue-600 dark:text-blue-400';
+      case 'queued':
+        return 'text-yellow-600 dark:text-yellow-400';
+      case 'cancelled':
+        return 'text-gray-600 dark:text-gray-400';
+      default:
+        return 'text-gray-600 dark:text-gray-400';
+    }
+  };
+
+  const getStatusBadgeVariant = (status: string): 'default' | 'secondary' | 'destructive' | 'outline' => {
+    switch (status) {
+      case 'completed':
+        return 'default';
+      case 'failed':
+        return 'destructive';
+      case 'processing':
+        return 'secondary';
+      case 'queued':
+        return 'outline';
+      case 'cancelled':
+        return 'outline';
+      default:
+        return 'outline';
+    }
+  };
+
+  const formatDuration = (startTime: string, endTime?: string) => {
+    const start = new Date(startTime).getTime();
+    const end = endTime ? new Date(endTime).getTime() : Date.now();
+    const duration = Math.floor((end - start) / 1000);
+
+    if (duration < 60) return `${duration}s`;
+    if (duration < 3600) return `${Math.floor(duration / 60)}m ${duration % 60}s`;
+    return `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`;
+  };
+
+  const getJobType = (job: JobInfo) => {
+    // Try to determine job type from message or other properties
+    const message = job.message?.toLowerCase() || '';
+    if (message.includes('text2img') || message.includes('text to image')) return 'Text to Image';
+    if (message.includes('img2img') || message.includes('image to image')) return 'Image to Image';
+    if (message.includes('upscale') || message.includes('upscaler')) return 'Upscale';
+    if (message.includes('convert') || message.includes('conversion')) return 'Model Conversion';
+    return 'Unknown';
+  };
+
+  const getJobTypeIcon = (job: JobInfo) => {
+    const type = getJobType(job);
+    switch (type) {
+      case 'Text to Image':
+        return <Image className="h-4 w-4" />;
+      case 'Image to Image':
+        return <Image className="h-4 w-4" />;
+      case 'Upscale':
+        return <Zap className="h-4 w-4" />;
+      case 'Model Conversion':
+        return <Settings className="h-4 w-4" />;
+      default:
+        return <FileText className="h-4 w-4" />;
+    }
+  };
+
+  const extractParameters = (job: JobInfo) => {
+    // Try to extract parameters from message or other job properties
+    const params: Record<string, any> = {};
+
+    if (job.message) {
+      // Try to parse parameters from message (this is a simplified approach)
+      const message = job.message;
+      const promptMatch = message.match(/prompt[:\s]+(.+?)(?:\n|$)/i);
+      if (promptMatch) params.prompt = promptMatch[1].trim();
+
+      const stepsMatch = message.match(/steps[:\s]+(\d+)/i);
+      if (stepsMatch) params.steps = parseInt(stepsMatch[1]);
+
+      const cfgMatch = message.match(/cfg[:\s]+([\d.]+)/i);
+      if (cfgMatch) params.cfg_scale = parseFloat(cfgMatch[1]);
+
+      const sizeMatch = message.match(/(\d+)x(\d+)/);
+      if (sizeMatch) {
+        params.width = parseInt(sizeMatch[1]);
+        params.height = parseInt(sizeMatch[2]);
+      }
+    }
+
+    return params;
+  };
+
+  const copyParameters = (job: JobInfo) => {
+    const params = extractParameters(job);
+    const paramsText = Object.entries(params)
+      .map(([key, value]) => `${key}: ${value}`)
+      .join('\n');
+
+    navigator.clipboard.writeText(paramsText);
+    onCopyParameters?.(job);
+  };
+
+  return (
+    <div className="space-y-6">
+      {/* Queue Status */}
+      <Card>
+        <CardHeader>
+          <div className="flex items-center justify-between">
+            <div>
+              <CardTitle>Queue Status</CardTitle>
+              <CardDescription>
+                Current queue status and statistics
+              </CardDescription>
+            </div>
+            <div className="flex gap-2">
+              <Button
+                variant={viewMode === 'detailed' ? 'default' : 'outline'}
+                size="sm"
+                onClick={() => setViewMode('detailed')}
+              >
+                <Eye className="h-4 w-4 mr-2" />
+                Detailed
+              </Button>
+              <Button
+                variant={viewMode === 'compact' ? 'default' : 'outline'}
+                size="sm"
+                onClick={() => setViewMode('compact')}
+              >
+                <Activity className="h-4 w-4 mr-2" />
+                Compact
+              </Button>
+              <Button onClick={onRefresh} disabled={loading}>
+                <RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
+                Refresh
+              </Button>
+              <Button variant="outline" onClick={onClearQueue} disabled={actionLoading || !queueStatus?.jobs.length}>
+                <Trash2 className="h-4 w-4 mr-2" />
+                Clear Queue
+              </Button>
+            </div>
+          </div>
+        </CardHeader>
+        <CardContent>
+          <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
+            <div className="text-center">
+              <div className="text-2xl font-bold">{queueStats.total}</div>
+              <div className="text-sm text-muted-foreground">Total Jobs</div>
+            </div>
+            <div className="text-center">
+              <div className="text-2xl font-bold text-blue-600">{queueStats.active}</div>
+              <div className="text-sm text-muted-foreground">Active</div>
+            </div>
+            <div className="text-center">
+              <div className="text-2xl font-bold text-yellow-600">{queueStats.queued}</div>
+              <div className="text-sm text-muted-foreground">Queued</div>
+            </div>
+            <div className="text-center">
+              <div className="text-2xl font-bold text-green-600">{queueStats.completed}</div>
+              <div className="text-sm text-muted-foreground">Completed</div>
+            </div>
+            <div className="text-center">
+              <div className="text-2xl font-bold text-red-600">{queueStats.failed}</div>
+              <div className="text-sm text-muted-foreground">Failed</div>
+            </div>
+          </div>
+        </CardContent>
+      </Card>
+
+      {/* Jobs List */}
+      <div className="space-y-4">
+        {sortedJobs.map(job => (
+          <Card key={job.id || job.request_id} className={cn(
+            "transition-all hover:shadow-md",
+            job.status === 'processing' && 'border-blue-200 bg-blue-50/50 dark:border-blue-800 dark:bg-blue-950/20',
+            job.status === 'completed' && 'border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20',
+            job.status === 'failed' && 'border-red-200 bg-red-50/50 dark:border-red-800 dark:bg-red-950/20'
+          )}>
+            <CardContent className="p-6">
+              {viewMode === 'detailed' ? (
+                <div className="space-y-4">
+                  {/* Header */}
+                  <div className="flex items-center justify-between">
+                    <div className="flex items-center gap-3">
+                      {getStatusIcon(job.status)}
+                      <div>
+                        <h3 className="font-semibold">Job {job.id || job.request_id}</h3>
+                        <div className="flex items-center gap-2 text-sm text-muted-foreground">
+                          {getJobTypeIcon(job)}
+                          <span>{getJobType(job)}</span>
+                          {job.queue_position !== undefined && (
+                            <span>• Position: {job.queue_position}</span>
+                          )}
+                        </div>
+                      </div>
+                    </div>
+                    <div className="flex items-center gap-2">
+                      <Badge variant={getStatusBadgeVariant(job.status)}>
+                        {job.status}
+                      </Badge>
+                      {(job.status === 'queued' || job.status === 'processing') && (
+                        <Button
+                          variant="outline"
+                          size="sm"
+                          onClick={() => onCancelJob(job.id || job.request_id!)}
+                          disabled={actionLoading}
+                        >
+                          <XCircle className="h-4 w-4" />
+                          Cancel
+                        </Button>
+                      )}
+                    </div>
+                  </div>
+
+                  {/* Progress */}
+                  {job.progress !== undefined && (
+                    <div className="space-y-2">
+                      <div className="flex items-center justify-between text-sm">
+                        <span className={cn("font-medium", getStatusColor(job.status))}>
+                          {job.message || 'Processing...'}
+                        </span>
+                        <span className="text-muted-foreground">
+                          {Math.round(job.progress * 100)}%
+                        </span>
+                      </div>
+                      <Progress value={job.progress} className="h-2" />
+                    </div>
+                  )}
+
+                  {/* Details Grid */}
+                  <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
+                    <div>
+                      <span className="text-muted-foreground flex items-center gap-1">
+                        <Calendar className="h-3 w-3" />
+                        Created
+                      </span>
+                      <p className="font-medium">
+                        {job.created_at ? new Date(job.created_at).toLocaleString() : 'Unknown'}
+                      </p>
+                    </div>
+                    {job.updated_at && job.updated_at !== job.created_at && (
+                      <div>
+                        <span className="text-muted-foreground flex items-center gap-1">
+                          <Clock className="h-3 w-3" />
+                          Updated
+                        </span>
+                        <p className="font-medium">
+                          {new Date(job.updated_at).toLocaleString()}
+                        </p>
+                      </div>
+                    )}
+                    {job.created_at && (
+                      <div>
+                        <span className="text-muted-foreground flex items-center gap-1">
+                          <Clock className="h-3 w-3" />
+                          Duration
+                        </span>
+                        <p className="font-medium">
+                          {formatDuration(job.created_at, job.updated_at)}
+                        </p>
+                      </div>
+                    )}
+                    <div>
+                      <span className="text-muted-foreground flex items-center gap-1">
+                        <FileText className="h-3 w-3" />
+                        Parameters
+                      </span>
+                      <div className="flex gap-1">
+                        <Button
+                          variant="ghost"
+                          size="sm"
+                          className="h-6 px-2 text-xs"
+                          onClick={() => copyParameters(job)}
+                        >
+                          <Copy className="h-3 w-3 mr-1" />
+                          Copy
+                        </Button>
+                        <Button
+                          variant="ghost"
+                          size="sm"
+                          className="h-6 px-2 text-xs"
+                          onClick={() => setSelectedJob(selectedJob === (job.id || job.request_id) ? null : (job.id || job.request_id || null))}
+                        >
+                          {selectedJob === (job.id || job.request_id) ? 'Hide' : 'Show'}
+                        </Button>
+                      </div>
+                    </div>
+                  </div>
+
+                  {/* Expanded Parameters */}
+                  {selectedJob === (job.id || job.request_id) && (
+                    <div className="bg-muted/50 rounded-lg p-4 space-y-2">
+                      <h4 className="font-medium text-sm">Job Parameters</h4>
+                      <div className="grid gap-2 text-sm">
+                        {Object.entries(extractParameters(job)).map(([key, value]) => (
+                          <div key={key} className="flex justify-between">
+                            <span className="text-muted-foreground capitalize">{key}:</span>
+                            <span className="font-mono text-xs">{String(value)}</span>
+                          </div>
+                        ))}
+                        {Object.keys(extractParameters(job)).length === 0 && (
+                          <p className="text-muted-foreground text-sm">No parameters available</p>
+                        )}
+                      </div>
+                    </div>
+                  )}
+
+                  {/* Results */}
+                  {job.status === 'completed' && job.result?.images && (
+                    <div className="space-y-2">
+                      <h4 className="font-medium text-sm flex items-center gap-1">
+                        <Image className="h-4 w-4" />
+                        Generated Images ({job.result.images.length})
+                      </h4>
+                      <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
+                        {job.result.images.map((imageData, index) => (
+                          <div key={index} className="relative group">
+                            <div className="aspect-square bg-muted rounded-lg overflow-hidden">
+                              {imageData.startsWith('data:image') ? (
+                                <img
+                                  src={imageData}
+                                  alt={`Generated image ${index + 1}`}
+                                  className="w-full h-full object-cover"
+                                />
+                              ) : (
+                                <div className="w-full h-full flex items-center justify-center text-muted-foreground">
+                                  <Image className="h-8 w-8" />
+                                </div>
+                              )}
+                            </div>
+                            <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center">
+                              <Button
+                                variant="secondary"
+                                size="sm"
+                                onClick={() => {
+                                  if (imageData.startsWith('data:image')) {
+                                    const link = document.createElement('a');
+                                    link.href = imageData;
+                                    link.download = `generated-image-${index + 1}.png`;
+                                    link.click();
+                                  }
+                                }}
+                              >
+                                Download
+                              </Button>
+                            </div>
+                          </div>
+                        ))}
+                      </div>
+                    </div>
+                  )}
+
+                  {/* Error */}
+                  {job.error && (
+                    <div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
+                      <p className="text-sm text-red-700 dark:text-red-300">
+                        <strong>Error:</strong> {job.error}
+                      </p>
+                    </div>
+                  )}
+                </div>
+              ) : (
+                /* Compact View */
+                <div className="flex items-center justify-between">
+                  <div className="flex items-center gap-4">
+                    {getStatusIcon(job.status)}
+                    <div>
+                      <h3 className="font-semibold">Job {job.id || job.request_id}</h3>
+                      <div className="flex items-center gap-2 text-sm text-muted-foreground">
+                        {getJobTypeIcon(job)}
+                        <span>{getJobType(job)}</span>
+                        <span>•</span>
+                        <span className={getStatusColor(job.status)}>{job.status}</span>
+                        {job.queue_position !== undefined && (
+                          <span>• Position: {job.queue_position}</span>
+                        )}
+                        {job.created_at && (
+                          <span>• {new Date(job.created_at).toLocaleTimeString()}</span>
+                        )}
+                      </div>
+                      {job.message && (
+                        <p className="text-sm text-muted-foreground truncate max-w-md">
+                          {job.message}
+                        </p>
+                      )}
+                    </div>
+                  </div>
+                  <div className="flex items-center gap-4">
+                    {job.progress !== undefined && (
+                      <div className="text-right">
+                        <div className="text-sm font-medium">
+                          {Math.round(job.progress * 100)}%
+                        </div>
+                        <div className="w-24 h-1.5 bg-gray-200 rounded-full overflow-hidden">
+                          <div
+                            className="h-full bg-blue-500 transition-all duration-300"
+                            style={{ width: `${job.progress * 100}%` }}
+                          />
+                        </div>
+                      </div>
+                    )}
+                    {(job.status === 'queued' || job.status === 'processing') && (
+                      <Button
+                        variant="outline"
+                        size="sm"
+                        onClick={() => onCancelJob(job.id || job.request_id!)}
+                        disabled={actionLoading}
+                      >
+                        <XCircle className="h-4 w-4" />
+                      </Button>
+                    )}
+                  </div>
+                </div>
+              )}
+            </CardContent>
+          </Card>
+        ))}
+      </div>
+
+      {(!queueStatus?.jobs || queueStatus.jobs.length === 0) && (
+        <Card>
+          <CardContent className="text-center py-12">
+            <Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
+            <p className="text-muted-foreground">No jobs in the queue.</p>
+          </CardContent>
+        </Card>
+      )}
+    </div>
+  );
+}

+ 6 - 2
webui/components/header.tsx

@@ -5,9 +5,10 @@ import { ThemeToggle } from './theme-toggle';
 interface HeaderProps {
   title: string;
   description?: string;
+  actions?: React.ReactNode;
 }
 
-export function Header({ title, description }: HeaderProps) {
+export function Header({ title, description, actions }: HeaderProps) {
   return (
     <header className="sticky top-0 z-30 flex h-16 items-center gap-4 border-b border-border bg-background px-6">
       <div className="flex-1">
@@ -16,7 +17,10 @@ export function Header({ title, description }: HeaderProps) {
           <p className="text-sm text-muted-foreground">{description}</p>
         )}
       </div>
-      <ThemeToggle />
+      <div className="flex items-center gap-4">
+        {actions}
+        <ThemeToggle />
+      </div>
     </header>
   );
 }

+ 291 - 0
webui/components/model-conversion-progress.tsx

@@ -0,0 +1,291 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Progress } from '@/components/ui/progress';
+import { Badge } from '@/components/ui/badge';
+import { apiClient, type JobInfo } from '@/lib/api';
+import {
+  Loader2,
+  CheckCircle2,
+  XCircle,
+  Clock,
+  AlertCircle,
+  RefreshCw,
+  Eye,
+  Trash2
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+interface ModelConversionProgressProps {
+  requestId: string;
+  modelName: string;
+  quantizationType: string;
+  onComplete?: () => void;
+  onCancel?: () => void;
+}
+
+interface ConversionStatus {
+  status: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled';
+  progress: number;
+  message: string;
+  error?: string;
+  createdAt: string;
+  updatedAt: string;
+  outputPath?: string;
+}
+
+export function ModelConversionProgress({
+  requestId,
+  modelName,
+  quantizationType,
+  onComplete,
+  onCancel
+}: ModelConversionProgressProps) {
+  const [conversionStatus, setConversionStatus] = useState<ConversionStatus | null>(null);
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState<string | null>(null);
+  const [polling, setPolling] = useState(true);
+
+  useEffect(() => {
+    if (!polling) return;
+
+    const checkStatus = async () => {
+      try {
+        // Try to get job status from queue
+        const queueStatus = await apiClient.getQueueStatus();
+        const job = queueStatus.jobs.find(j => j.request_id === requestId || j.id === requestId);
+
+        if (job) {
+          const status: ConversionStatus = {
+            status: job.status as any,
+            progress: job.progress || 0,
+            message: job.message || 'Processing...',
+            error: job.error,
+            createdAt: job.created_at || new Date().toISOString(),
+            updatedAt: job.updated_at || new Date().toISOString(),
+          };
+
+          setConversionStatus(status);
+          setError(null);
+
+          // Stop polling if completed or failed
+          if (job.status === 'completed') {
+            setPolling(false);
+            onComplete?.();
+          } else if (job.status === 'failed' || job.status === 'cancelled') {
+            setPolling(false);
+            setError(job.error || 'Conversion failed');
+          }
+        } else {
+          // Job not found in queue, might be completed or failed
+          if (conversionStatus?.status !== 'completed') {
+            setPolling(false);
+            setError('Conversion job not found in queue');
+          }
+        }
+      } catch (err) {
+        console.error('Failed to check conversion status:', err);
+        setError('Failed to check status');
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    checkStatus();
+
+    // Poll every 2 seconds when active
+    const interval = polling ? setInterval(checkStatus, 2000) : null;
+    return () => {
+      if (interval) clearInterval(interval);
+    };
+  }, [requestId, polling, onComplete, conversionStatus?.status]);
+
+  const getStatusIcon = () => {
+    if (loading) return <Loader2 className="h-5 w-5 animate-spin" />;
+
+    switch (conversionStatus?.status) {
+      case 'completed':
+        return <CheckCircle2 className="h-5 w-5 text-green-500" />;
+      case 'failed':
+        return <XCircle className="h-5 w-5 text-red-500" />;
+      case 'processing':
+        return <Loader2 className="h-5 w-5 text-blue-500 animate-spin" />;
+      case 'cancelled':
+        return <XCircle className="h-5 w-5 text-gray-500" />;
+      case 'pending':
+        return <Clock className="h-5 w-5 text-yellow-500" />;
+      default:
+        return <AlertCircle className="h-5 w-5 text-gray-500" />;
+    }
+  };
+
+  const getStatusColor = () => {
+    switch (conversionStatus?.status) {
+      case 'completed':
+        return 'text-green-600 dark:text-green-400';
+      case 'failed':
+        return 'text-red-600 dark:text-red-400';
+      case 'processing':
+        return 'text-blue-600 dark:text-blue-400';
+      case 'cancelled':
+        return 'text-gray-600 dark:text-gray-400';
+      case 'pending':
+        return 'text-yellow-600 dark:text-yellow-400';
+      default:
+        return 'text-gray-600 dark:text-gray-400';
+    }
+  };
+
+  const getStatusBadgeVariant = () => {
+    switch (conversionStatus?.status) {
+      case 'completed':
+        return 'default';
+      case 'failed':
+        return 'destructive';
+      case 'processing':
+        return 'secondary';
+      case 'cancelled':
+        return 'outline';
+      case 'pending':
+        return 'outline';
+      default:
+        return 'outline';
+    }
+  };
+
+  if (error && !conversionStatus) {
+    return (
+      <Card className="border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/20">
+        <CardContent className="p-6">
+          <div className="flex items-center gap-3">
+            <XCircle className="h-5 w-5 text-red-500" />
+            <div>
+              <h3 className="font-semibold text-red-900 dark:text-red-100">Conversion Error</h3>
+              <p className="text-sm text-red-700 dark:text-red-300">{error}</p>
+            </div>
+          </div>
+        </CardContent>
+      </Card>
+    );
+  }
+
+  return (
+    <Card>
+      <CardHeader>
+        <div className="flex items-center justify-between">
+          <div>
+            <CardTitle className="flex items-center gap-2">
+              {getStatusIcon()}
+              Model Conversion
+            </CardTitle>
+            <CardDescription>
+              Converting {modelName} to {quantizationType}
+            </CardDescription>
+          </div>
+          <Badge variant={getStatusBadgeVariant()}>
+            {conversionStatus?.status || 'Unknown'}
+          </Badge>
+        </div>
+      </CardHeader>
+      <CardContent className="space-y-4">
+        <div className="space-y-2">
+          <div className="flex items-center justify-between text-sm">
+            <span className={cn("font-medium", getStatusColor())}>
+              {conversionStatus?.message || 'Initializing...'}
+            </span>
+            <span className="text-muted-foreground">
+              {conversionStatus ? Math.round(conversionStatus.progress * 100) : 0}%
+            </span>
+          </div>
+          <Progress
+            value={conversionStatus?.progress || 0}
+            className="h-2"
+          />
+        </div>
+
+        <div className="grid grid-cols-2 gap-4 text-sm">
+          <div>
+            <span className="text-muted-foreground">Model:</span>
+            <p className="font-medium">{modelName}</p>
+          </div>
+          <div>
+            <span className="text-muted-foreground">Quantization:</span>
+            <p className="font-medium">{quantizationType}</p>
+          </div>
+          {conversionStatus?.createdAt && (
+            <div>
+              <span className="text-muted-foreground">Started:</span>
+              <p className="font-medium">
+                {new Date(conversionStatus.createdAt).toLocaleTimeString()}
+              </p>
+            </div>
+          )}
+          {conversionStatus?.updatedAt && (
+            <div>
+              <span className="text-muted-foreground">Updated:</span>
+              <p className="font-medium">
+                {new Date(conversionStatus.updatedAt).toLocaleTimeString()}
+              </p>
+            </div>
+          )}
+        </div>
+
+        {conversionStatus?.error && (
+          <div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
+            <p className="text-sm text-red-700 dark:text-red-300">
+              <strong>Error:</strong> {conversionStatus.error}
+            </p>
+          </div>
+        )}
+
+        {conversionStatus?.status === 'completed' && (
+          <div className="bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-lg p-3">
+            <p className="text-sm text-green-700 dark:text-green-300">
+              <strong>Success!</strong> Model conversion completed successfully.
+            </p>
+            {conversionStatus.outputPath && (
+              <p className="text-xs text-green-600 dark:text-green-400 mt-1">
+                Output: {conversionStatus.outputPath}
+              </p>
+            )}
+          </div>
+        )}
+
+        <div className="flex gap-2 pt-2">
+          {polling && (
+            <Button
+              variant="outline"
+              size="sm"
+              onClick={() => setPolling(false)}
+            >
+              <Eye className="h-4 w-4 mr-2" />
+              Stop Watching
+            </Button>
+          )}
+          {!polling && conversionStatus?.status === 'processing' && (
+            <Button
+              variant="outline"
+              size="sm"
+              onClick={() => setPolling(true)}
+            >
+              <RefreshCw className="h-4 w-4 mr-2" />
+              Resume Watching
+            </Button>
+          )}
+          {(conversionStatus?.status === 'failed' || conversionStatus?.status === 'cancelled') && (
+            <Button
+              variant="outline"
+              size="sm"
+              onClick={onCancel}
+            >
+              <Trash2 className="h-4 w-4 mr-2" />
+              Close
+            </Button>
+          )}
+        </div>
+      </CardContent>
+    </Card>
+  );
+}

+ 403 - 0
webui/components/model-list.tsx

@@ -0,0 +1,403 @@
+'use client';
+
+import { useState, useMemo } from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { type ModelInfo } from '@/lib/api';
+import {
+  Search,
+  RefreshCw,
+  Download,
+  Trash2,
+  CheckCircle2,
+  XCircle,
+  Loader2,
+  HardDrive,
+  FileText,
+  Zap,
+  Copy,
+  MoreHorizontal,
+  Eye,
+  EyeOff,
+  Filter,
+  Grid3X3,
+  List
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+interface ModelListProps {
+  models: ModelInfo[];
+  loading: boolean;
+  onRefresh: () => void;
+  onLoadModel: (modelId: string) => void;
+  onUnloadModel: (modelId: string) => void;
+  onConvertModel?: (modelName: string, quantizationType: string) => void;
+  actionLoading: string | null;
+}
+
+type ViewMode = 'grid' | 'list';
+
+export function ModelList({
+  models,
+  loading,
+  onRefresh,
+  onLoadModel,
+  onUnloadModel,
+  onConvertModel,
+  actionLoading
+}: ModelListProps) {
+  const [searchTerm, setSearchTerm] = useState('');
+  const [selectedType, setSelectedType] = useState<string>('all');
+  const [showFullPaths, setShowFullPaths] = useState(false);
+  const [viewMode, setViewMode] = useState<ViewMode>('grid');
+
+  // Calculate model type statistics
+  const modelStats = useMemo(() => {
+    const stats = models.reduce((acc, model) => {
+      const type = model.type || 'unknown';
+      acc[type] = (acc[type] || 0) + 1;
+      return acc;
+    }, {} as Record<string, number>);
+
+    return {
+      total: models.length,
+      loaded: models.filter(m => m.loaded).length,
+      types: Object.entries(stats).sort(([a], [b]) => a.localeCompare(b))
+    };
+  }, [models]);
+
+  // Filter models
+  const filteredModels = useMemo(() => {
+    return models.filter(model => {
+      const matchesSearch = model.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+                           (model.sha256_short && model.sha256_short.toLowerCase().includes(searchTerm.toLowerCase()));
+      const matchesType = selectedType === 'all' || model.type === selectedType;
+      return matchesSearch && matchesType;
+    });
+  }, [models, searchTerm, selectedType]);
+
+  // Get display name (compact vs full path)
+  const getDisplayName = (model: ModelInfo) => {
+    if (showFullPaths && model.path) {
+      return model.path;
+    }
+    return model.name;
+  };
+
+  // Get model type icon
+  const getTypeIcon = (type: string) => {
+    switch (type.toLowerCase()) {
+      case 'stable-diffusion':
+      case 'sd':
+        return <Image className="h-4 w-4" />;
+      case 'vae':
+        return <Zap className="h-4 w-4" />;
+      case 'textual-inversion':
+      case 'embedding':
+        return <FileText className="h-4 w-4" />;
+      default:
+        return <HardDrive className="h-4 w-4" />;
+    }
+  };
+
+  // Format file size
+  const formatFileSize = (size?: number) => {
+    if (!size) return 'Unknown';
+    if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
+    return `${(size / (1024 * 1024)).toFixed(1)} MB`;
+  };
+
+  return (
+    <div className="space-y-6">
+      {/* Controls */}
+      <Card>
+        <CardHeader>
+          <div className="flex items-center justify-between">
+            <div>
+              <CardTitle>Model Management</CardTitle>
+              <CardDescription>
+                {modelStats.total} models total • {modelStats.loaded} loaded
+              </CardDescription>
+            </div>
+            <div className="flex gap-2">
+              <Button
+                variant={viewMode === 'grid' ? 'default' : 'outline'}
+                size="sm"
+                onClick={() => setViewMode('grid')}
+              >
+                <Grid3X3 className="h-4 w-4" />
+              </Button>
+              <Button
+                variant={viewMode === 'list' ? 'default' : 'outline'}
+                size="sm"
+                onClick={() => setViewMode('list')}
+              >
+                <List className="h-4 w-4" />
+              </Button>
+              <Button onClick={onRefresh} disabled={loading}>
+                <RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
+                Refresh
+              </Button>
+            </div>
+          </div>
+        </CardHeader>
+        <CardContent className="space-y-4">
+          <div className="flex gap-4">
+            <div className="flex-1">
+              <Label htmlFor="search">Search Models</Label>
+              <div className="relative">
+                <Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
+                <Input
+                  id="search"
+                  placeholder="Search by name or hash..."
+                  value={searchTerm}
+                  onChange={(e) => setSearchTerm(e.target.value)}
+                  className="pl-10"
+                />
+              </div>
+            </div>
+            <div className="flex items-end gap-2">
+              <Button
+                variant="outline"
+                size="sm"
+                onClick={() => setShowFullPaths(!showFullPaths)}
+              >
+                {showFullPaths ? <EyeOff className="h-4 w-4 mr-2" /> : <Eye className="h-4 w-4 mr-2" />}
+                {showFullPaths ? 'Hide Paths' : 'Show Paths'}
+              </Button>
+            </div>
+          </div>
+
+          {/* Model Type Counters */}
+          <div className="space-y-2">
+            <div className="flex items-center gap-2">
+              <Filter className="h-4 w-4" />
+              <span className="text-sm font-medium">Filter by type:</span>
+            </div>
+            <div className="flex gap-2 flex-wrap">
+              <Button
+                variant={selectedType === 'all' ? 'default' : 'outline'}
+                size="sm"
+                onClick={() => setSelectedType('all')}
+              >
+                All ({modelStats.total})
+              </Button>
+              {modelStats.types.map(([type, count]) => (
+                <Button
+                  key={type}
+                  variant={selectedType === type ? 'default' : 'outline'}
+                  size="sm"
+                  onClick={() => setSelectedType(type)}
+                  className="flex items-center gap-2"
+                >
+                  {getTypeIcon(type)}
+                  {type} ({count})
+                </Button>
+              ))}
+            </div>
+          </div>
+        </CardContent>
+      </Card>
+
+      {/* Models Grid/List */}
+      {viewMode === 'grid' ? (
+        <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
+          {filteredModels.map(model => (
+            <Card key={model.id || model.name} className={cn(
+              'transition-all hover:shadow-md',
+              model.loaded ? 'border-green-500 bg-green-50/50 dark:bg-green-950/20' : ''
+            )}>
+              <CardContent className="p-4">
+                <div className="space-y-3">
+                  <div className="flex items-start justify-between">
+                    <div className="flex items-center gap-2">
+                      {model.loaded ? (
+                        <CheckCircle2 className="h-4 w-4 text-green-500" />
+                      ) : (
+                        <XCircle className="h-4 w-4 text-muted-foreground" />
+                      )}
+                      <div className="flex items-center gap-1">
+                        {getTypeIcon(model.type)}
+                        <span className="text-xs text-muted-foreground">{model.type}</span>
+                      </div>
+                    </div>
+                  </div>
+
+                  <div className="space-y-1">
+                    <h3 className="font-semibold text-sm truncate" title={getDisplayName(model)}>
+                      {getDisplayName(model)}
+                    </h3>
+                    <div className="flex items-center gap-2 text-xs text-muted-foreground">
+                      <HardDrive className="h-3 w-3" />
+                      <span>{formatFileSize(model.size || model.file_size)}</span>
+                    </div>
+                    {model.sha256_short && (
+                      <div className="flex items-center gap-1">
+                        <span className="text-xs text-muted-foreground font-mono">
+                          {model.sha256_short}
+                        </span>
+                        <Button
+                          variant="ghost"
+                          size="sm"
+                          className="h-4 w-4 p-0"
+                          onClick={() => navigator.clipboard.writeText(model.sha256_short!)}
+                        >
+                          <Copy className="h-3 w-3" />
+                        </Button>
+                      </div>
+                    )}
+                  </div>
+
+                  <div className="flex gap-2 pt-2">
+                    {model.loaded ? (
+                      <Button
+                        variant="outline"
+                        size="sm"
+                        className="flex-1"
+                        onClick={() => onUnloadModel(model.id || model.name)}
+                        disabled={actionLoading === model.id}
+                      >
+                        {actionLoading === model.id ? (
+                          <Loader2 className="h-3 w-3 animate-spin" />
+                        ) : (
+                          <Trash2 className="h-3 w-3" />
+                        )}
+                        Unload
+                      </Button>
+                    ) : (
+                      <Button
+                        variant="default"
+                        size="sm"
+                        className="flex-1"
+                        onClick={() => onLoadModel(model.id || model.name)}
+                        disabled={actionLoading === model.id}
+                      >
+                        {actionLoading === model.id ? (
+                          <Loader2 className="h-3 w-3 animate-spin" />
+                        ) : (
+                          <Download className="h-3 w-3" />
+                        )}
+                        Load
+                      </Button>
+                    )}
+                    {onConvertModel && (
+                      <Button
+                        variant="outline"
+                        size="sm"
+                        onClick={() => onConvertModel(model.name, 'q4_0')}
+                        disabled={actionLoading === model.id}
+                      >
+                        <Zap className="h-3 w-3" />
+                        Convert
+                      </Button>
+                    )}
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          ))}
+        </div>
+      ) : (
+        <div className="space-y-2">
+          {filteredModels.map(model => (
+            <Card key={model.id || model.name} className={cn(
+              'transition-all hover:shadow-md',
+              model.loaded ? 'border-green-500 bg-green-50/50 dark:bg-green-950/20' : ''
+            )}>
+              <CardContent className="p-4">
+                <div className="flex items-center justify-between">
+                  <div className="flex items-center gap-4">
+                    <div className="flex items-center gap-2">
+                      {model.loaded ? (
+                        <CheckCircle2 className="h-5 w-5 text-green-500" />
+                      ) : (
+                        <XCircle className="h-5 w-5 text-muted-foreground" />
+                      )}
+                      <div className="flex items-center gap-2">
+                        {getTypeIcon(model.type)}
+                        <div>
+                          <h3 className="font-semibold">{getDisplayName(model)}</h3>
+                          <div className="flex items-center gap-4 text-sm text-muted-foreground">
+                            <span>{model.type}</span>
+                            <span>{formatFileSize(model.size || model.file_size)}</span>
+                            {model.sha256_short && (
+                              <span className="font-mono">{model.sha256_short}</span>
+                            )}
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                  <div className="flex gap-2">
+                    {model.sha256_short && (
+                      <Button
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => navigator.clipboard.writeText(model.sha256_short!)}
+                      >
+                        <Copy className="h-4 w-4" />
+                      </Button>
+                    )}
+                    {model.loaded ? (
+                      <Button
+                        variant="outline"
+                        size="sm"
+                        onClick={() => onUnloadModel(model.id || model.name)}
+                        disabled={actionLoading === model.id}
+                      >
+                        {actionLoading === model.id ? (
+                          <Loader2 className="h-4 w-4 animate-spin" />
+                        ) : (
+                          <Trash2 className="h-4 w-4" />
+                        )}
+                        Unload
+                      </Button>
+                    ) : (
+                      <Button
+                        variant="default"
+                        size="sm"
+                        onClick={() => onLoadModel(model.id || model.name)}
+                        disabled={actionLoading === model.id}
+                      >
+                        {actionLoading === model.id ? (
+                          <Loader2 className="h-4 w-4 animate-spin" />
+                        ) : (
+                          <Download className="h-4 w-4" />
+                        )}
+                        Load
+                      </Button>
+                    )}
+                    {onConvertModel && (
+                      <Button
+                        variant="outline"
+                        size="sm"
+                        onClick={() => onConvertModel(model.name, 'q4_0')}
+                        disabled={actionLoading === model.id}
+                      >
+                        <Zap className="h-4 w-4" />
+                        Convert
+                      </Button>
+                    )}
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          ))}
+        </div>
+      )}
+
+      {filteredModels.length === 0 && (
+        <Card>
+          <CardContent className="text-center py-12">
+            <p className="text-muted-foreground">No models found matching your criteria.</p>
+          </CardContent>
+        </Card>
+      )}
+    </div>
+  );
+}
+
+// Import Image icon that was missing
+import { Image } from 'lucide-react';

+ 59 - 0
webui/components/ui/alert.tsx

@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+  "relative w-full rounded-lg border p-4 [&>svg~*]:text-foreground",
+  {
+    variants: {
+      variant: {
+        default: "bg-background text-foreground",
+        destructive:
+          "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+    },
+  }
+)
+
+const Alert = React.forwardRef<
+  HTMLDivElement,
+  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
+>(({ className, variant, ...props }, ref) => (
+  <div
+    ref={ref}
+    role="alert"
+    className={cn(alertVariants({ variant }), className)}
+    {...props}
+  />
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+  HTMLParagraphElement,
+  React.HTMLAttributes<HTMLHeadingElement>
+>(({ className, ...props }, ref) => (
+  <h5
+    ref={ref}
+    className={cn("mb-1 font-medium leading-none tracking-tight", className)}
+    {...props}
+  />
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+  HTMLParagraphElement,
+  React.HTMLAttributes<HTMLParagraphElement>
+>(({ className, ...props }, ref) => (
+  <div
+    ref={ref}
+    className={cn("text-sm [&_p]:leading-relaxed [&_p:not(:first-child)]:mt-2", className)}
+    {...props}
+  />
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }

+ 36 - 0
webui/components/ui/badge.tsx

@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cn } from "@/lib/utils"
+
+export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
+  variant?: 'default' | 'secondary' | 'destructive' | 'outline'
+}
+
+function Badge({ className, variant = 'default', ...props }: BadgeProps) {
+  const getVariantClasses = () => {
+    switch (variant) {
+      case 'default':
+        return "border-transparent bg-primary text-primary-foreground hover:bg-primary/80"
+      case 'secondary':
+        return "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80"
+      case 'destructive':
+        return "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80"
+      case 'outline':
+        return "text-foreground"
+      default:
+        return "border-transparent bg-primary text-primary-foreground hover:bg-primary/80"
+    }
+  }
+
+  return (
+    <div
+      className={cn(
+        "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+        getVariantClasses(),
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Badge }

+ 29 - 0
webui/components/ui/progress.tsx

@@ -0,0 +1,29 @@
+"use client"
+
+import * as React from "react"
+import { cn } from "@/lib/utils"
+
+interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
+  value?: number
+}
+
+const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
+  ({ className, value, ...props }, ref) => (
+    <div
+      ref={ref}
+      className={cn(
+        "relative h-4 w-full overflow-hidden rounded-full bg-secondary",
+        className
+      )}
+      {...props}
+    >
+      <div
+        className="h-full w-full flex-1 bg-primary transition-all"
+        style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
+      />
+    </div>
+  )
+)
+Progress.displayName = "Progress"
+
+export { Progress }

+ 55 - 0
webui/components/ui/tabs.tsx

@@ -0,0 +1,55 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+  React.ElementRef<typeof TabsPrimitive.List>,
+  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
+>(({ className, ...props }, ref) => (
+  <TabsPrimitive.List
+    ref={ref}
+    className={cn(
+      "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
+      className
+    )}
+    {...props}
+  />
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+  React.ElementRef<typeof TabsPrimitive.Trigger>,
+  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
+>(({ className, ...props }, ref) => (
+  <TabsPrimitive.Trigger
+    ref={ref}
+    className={cn(
+      "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
+      className
+    )}
+    {...props}
+  />
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+  React.ElementRef<typeof TabsPrimitive.Content>,
+  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
+>(({ className, ...props }, ref) => (
+  <TabsPrimitive.Content
+    ref={ref}
+    className={cn(
+      "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
+      className
+    )}
+    {...props}
+  />
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }

+ 191 - 4
webui/lib/api.ts

@@ -93,12 +93,18 @@ class ApiClient {
     options: RequestInit = {}
   ): Promise<T> {
     const url = `${this.getBaseUrl()}${endpoint}`;
+
+    // Add auth token if available
+    const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
+    const headers = {
+      'Content-Type': 'application/json',
+      ...(token && { 'Authorization': `Bearer ${token}` }),
+      ...options.headers,
+    };
+
     const response = await fetch(url, {
       ...options,
-      headers: {
-        'Content-Type': 'application/json',
-        ...options.headers,
-      },
+      headers,
     });
 
     if (!response.ok) {
@@ -249,4 +255,185 @@ class ApiClient {
   }
 }
 
+// Generic API request function for authentication
+export async function apiRequest(
+  endpoint: string,
+  options: RequestInit = {}
+): Promise<Response> {
+  const { apiUrl, apiBase } = getApiConfig();
+  const url = `${apiUrl}${apiBase}${endpoint}`;
+
+  // Add auth token if available
+  const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
+  const headers = {
+    'Content-Type': 'application/json',
+    ...(token && { 'Authorization': `Bearer ${token}` }),
+    ...options.headers,
+  };
+
+  return fetch(url, {
+    ...options,
+    headers,
+  });
+}
+
+// Authentication API endpoints
+export const authApi = {
+  async login(username: string, password: string) {
+    const response = await apiRequest('/auth/login', {
+      method: 'POST',
+      body: JSON.stringify({ username, password }),
+    });
+
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({ message: 'Login failed' }));
+      throw new Error(error.message || 'Login failed');
+    }
+
+    return response.json();
+  },
+
+  async validateToken(token: string) {
+    const response = await apiRequest('/auth/validate', {
+      headers: { 'Authorization': `Bearer ${token}` },
+    });
+
+    if (!response.ok) {
+      throw new Error('Token validation failed');
+    }
+
+    return response.json();
+  },
+
+  async refreshToken() {
+    const response = await apiRequest('/auth/refresh', {
+      method: 'POST',
+    });
+
+    if (!response.ok) {
+      throw new Error('Token refresh failed');
+    }
+
+    return response.json();
+  },
+
+  async logout() {
+    await apiRequest('/auth/logout', {
+      method: 'POST',
+    });
+  },
+
+  async getCurrentUser() {
+    const response = await apiRequest('/auth/me');
+
+    if (!response.ok) {
+      throw new Error('Failed to get current user');
+    }
+
+    return response.json();
+  },
+
+  async changePassword(oldPassword: string, newPassword: string) {
+    const response = await apiRequest('/auth/change-password', {
+      method: 'POST',
+      body: JSON.stringify({ old_password: oldPassword, new_password: newPassword }),
+    });
+
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({ message: 'Password change failed' }));
+      throw new Error(error.message || 'Password change failed');
+    }
+
+    return response.json();
+  },
+
+  // API Key management
+  async getApiKeys() {
+    const response = await apiRequest('/auth/api-keys');
+
+    if (!response.ok) {
+      throw new Error('Failed to get API keys');
+    }
+
+    return response.json();
+  },
+
+  async createApiKey(name: string, scopes?: string[]) {
+    const response = await apiRequest('/auth/api-keys', {
+      method: 'POST',
+      body: JSON.stringify({ name, scopes }),
+    });
+
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({ message: 'Failed to create API key' }));
+      throw new Error(error.message || 'Failed to create API key');
+    }
+
+    return response.json();
+  },
+
+  async deleteApiKey(keyId: string) {
+    const response = await apiRequest(`/auth/api-keys/${keyId}`, {
+      method: 'DELETE',
+    });
+
+    if (!response.ok) {
+      throw new Error('Failed to delete API key');
+    }
+
+    return response.json();
+  },
+
+  // User management (admin only)
+  async getUsers() {
+    const response = await apiRequest('/auth/users');
+
+    if (!response.ok) {
+      throw new Error('Failed to get users');
+    }
+
+    return response.json();
+  },
+
+  async createUser(userData: { username: string; email?: string; password: string; role?: string }) {
+    const response = await apiRequest('/auth/users', {
+      method: 'POST',
+      body: JSON.stringify(userData),
+    });
+
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({ message: 'Failed to create user' }));
+      throw new Error(error.message || 'Failed to create user');
+    }
+
+    return response.json();
+  },
+
+  async updateUser(userId: string, userData: { email?: string; role?: string; active?: boolean }) {
+    const response = await apiRequest(`/auth/users/${userId}`, {
+      method: 'PUT',
+      body: JSON.stringify(userData),
+    });
+
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({ message: 'Failed to update user' }));
+      throw new Error(error.message || 'Failed to update user');
+    }
+
+    return response.json();
+  },
+
+  async deleteUser(userId: string) {
+    const response = await apiRequest(`/auth/users/${userId}`, {
+      method: 'DELETE',
+    });
+
+    if (!response.ok) {
+      throw new Error('Failed to delete user');
+    }
+
+    return response.json();
+  }
+};
+
 export const apiClient = new ApiClient();

+ 152 - 0
webui/lib/auth-context.tsx

@@ -0,0 +1,152 @@
+"use client"
+
+import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'
+import { authApi } from './api'
+
+interface User {
+  id: string
+  username: string
+  email?: string
+  role: 'admin' | 'user'
+  createdAt: string
+  lastLogin?: string
+}
+
+interface AuthState {
+  user: User | null
+  token: string | null
+  isAuthenticated: boolean
+  isLoading: boolean
+  error: string | null
+}
+
+interface AuthContextType extends AuthState {
+  login: (username: string, password: string) => Promise<void>
+  logout: () => void
+  refreshToken: () => Promise<void>
+  clearError: () => void
+}
+
+const AuthContext = createContext<AuthContextType | undefined>(undefined)
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+  const [state, setState] = useState<AuthState>({
+    user: null,
+    token: null,
+    isAuthenticated: false,
+    isLoading: true,
+    error: null
+  })
+
+  useEffect(() => {
+    // Check for existing token on mount
+    const token = localStorage.getItem('auth_token')
+    if (token) {
+      validateToken(token)
+    } else {
+      setState(prev => ({ ...prev, isLoading: false }))
+    }
+  }, [])
+
+  const validateToken = async (token: string) => {
+    try {
+      const data = await authApi.validateToken(token)
+      setState({
+        user: data.user,
+        token,
+        isAuthenticated: true,
+        isLoading: false,
+        error: null
+      })
+      localStorage.setItem('auth_token', token)
+    } catch (error) {
+      console.error('Token validation error:', error)
+      localStorage.removeItem('auth_token')
+      setState({
+        user: null,
+        token: null,
+        isAuthenticated: false,
+        isLoading: false,
+        error: 'Session expired. Please log in again.'
+      })
+    }
+  }
+
+  const login = async (username: string, password: string) => {
+    setState(prev => ({ ...prev, isLoading: true, error: null }))
+
+    try {
+      const data = await authApi.login(username, password)
+      const { token, user } = data
+
+      setState({
+        user,
+        token,
+        isAuthenticated: true,
+        isLoading: false,
+        error: null
+      })
+
+      localStorage.setItem('auth_token', token)
+    } catch (error) {
+      setState(prev => ({
+        ...prev,
+        isLoading: false,
+        error: error instanceof Error ? error.message : 'Login failed'
+      }))
+    }
+  }
+
+  const logout = () => {
+    localStorage.removeItem('auth_token')
+    setState({
+      user: null,
+      token: null,
+      isAuthenticated: false,
+      isLoading: false,
+      error: null
+    })
+  }
+
+  const refreshToken = async () => {
+    const { token } = state
+    if (!token) return
+
+    try {
+      const data = await authApi.refreshToken()
+      const newToken = data.token
+
+      setState(prev => ({ ...prev, token: newToken }))
+      localStorage.setItem('auth_token', newToken)
+    } catch (error) {
+      console.error('Token refresh error:', error)
+      logout()
+    }
+  }
+
+  const clearError = () => {
+    setState(prev => ({ ...prev, error: null }))
+  }
+
+  const value: AuthContextType = {
+    ...state,
+    login,
+    logout,
+    refreshToken,
+    clearError
+  }
+
+  return (
+    <AuthContext.Provider value={value}>
+      {children}
+    </AuthContext.Provider>
+  )
+}
+
+export function useAuth(): AuthContextType {
+  const context = useContext(AuthContext)
+  if (context === undefined) {
+    throw new Error('useAuth must be used within an AuthProvider')
+  }
+  return context
+}

+ 305 - 3
webui/package-lock.json

@@ -8,6 +8,8 @@
       "name": "webui",
       "version": "0.1.0",
       "dependencies": {
+        "@radix-ui/react-tabs": "^1.1.13",
+        "class-variance-authority": "^0.7.1",
         "clsx": "^2.1.1",
         "lucide-react": "^0.548.0",
         "next": "16.0.0",
@@ -1191,6 +1193,294 @@
         "node": ">=12.4.0"
       }
     },
+    "node_modules/@radix-ui/primitive": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+      "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+      "license": "MIT"
+    },
+    "node_modules/@radix-ui/react-collection": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+      "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-slot": "1.2.3"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-compose-refs": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+      "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-context": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+      "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-direction": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+      "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-id": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+      "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-presence": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+      "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-primitive": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+      "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-slot": "1.2.3"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-roving-focus": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+      "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.3",
+        "@radix-ui/react-collection": "1.1.7",
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-direction": "1.1.1",
+        "@radix-ui/react-id": "1.1.1",
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-use-callback-ref": "1.1.1",
+        "@radix-ui/react-use-controllable-state": "1.2.2"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-slot": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+      "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.2"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-tabs": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+      "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.3",
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-direction": "1.1.1",
+        "@radix-ui/react-id": "1.1.1",
+        "@radix-ui/react-presence": "1.1.5",
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-roving-focus": "1.1.11",
+        "@radix-ui/react-use-controllable-state": "1.2.2"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-use-callback-ref": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+      "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-use-controllable-state": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+      "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-use-effect-event": "0.0.2",
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-use-effect-event": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+      "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-use-layout-effect": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+      "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@rtsao/scc": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1524,7 +1814,7 @@
       "version": "19.2.2",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
       "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
-      "dev": true,
+      "devOptional": true,
       "license": "MIT",
       "dependencies": {
         "csstype": "^3.0.2"
@@ -1534,7 +1824,7 @@
       "version": "19.2.2",
       "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
       "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
-      "dev": true,
+      "devOptional": true,
       "license": "MIT",
       "peerDependencies": {
         "@types/react": "^19.2.0"
@@ -2568,6 +2858,18 @@
         "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
+    "node_modules/class-variance-authority": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+      "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "clsx": "^2.1.1"
+      },
+      "funding": {
+        "url": "https://polar.sh/cva"
+      }
+    },
     "node_modules/client-only": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -2636,7 +2938,7 @@
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
-      "dev": true,
+      "devOptional": true,
       "license": "MIT"
     },
     "node_modules/damerau-levenshtein": {

+ 2 - 0
webui/package.json

@@ -9,6 +9,8 @@
     "lint": "eslint"
   },
   "dependencies": {
+    "@radix-ui/react-tabs": "^1.1.13",
+    "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",
     "lucide-react": "^0.548.0",
     "next": "16.0.0",