package controllers import ( "context" "errors" "strings" "git.golem15.com/golem15/sm-golem-plugin/models" "git.golem15.com/golem15/summercms/modules/cabana" "git.golem15.com/golem15/summercms/modules/pact" ) // PermissionAccessSettings guards the AI models screen. const PermissionAccessSettings = "golem15.golem.access_settings" // ModelsAdminController is the AI models list and form // (golem15.golem.models). The API key is write-only: the form shows it // redacted, and an update that sends it back empty or redacted keeps the // stored key, unless the same update changes the adapter or base URL. type ModelsAdminController struct{} var ( _ pact.AdminController = ModelsAdminController{} _ pact.AdminPermissioned = ModelsAdminController{} _ pact.AdminRecordSource = ModelsAdminController{} _ pact.FormBeforeCreate = ModelsAdminController{} _ pact.FormBeforeUpdate = ModelsAdminController{} ) // ID is golem15.golem.models. func (ModelsAdminController) ID() string { return "golem15.golem.models" } // ModelName is the model's WinterCMS class name. func (ModelsAdminController) ModelName() string { return `Golem15\Golem\Models\AiModel` } // ConfigDir holds config_list.yaml and config_form.yaml. func (ModelsAdminController) ConfigDir() string { return "controllers/aimodels" } // RequiredPermissions is golem15.golem.access_settings. func (ModelsAdminController) RequiredPermissions() []string { return []string{PermissionAccessSettings} } // NewRecord returns an empty AI model. func (ModelsAdminController) NewRecord() any { return &models.AiModel{} } // redacted is how lagoon.Encrypted serializes, so how the form shows the // stored key. const redacted = "[redacted]" func keyMissing(m *models.AiModel) bool { k := m.APIKey.Reveal() return k == "" || k == redacted } // FormBeforeCreate requires an API key. func (ModelsAdminController) FormBeforeCreate(_ context.Context, model any) error { m, ok := model.(*models.AiModel) if !ok { return errors.New("golem: unexpected admin record") } if keyMissing(m) { return &cabana.ValidationError{Details: map[string]any{"api_key": []string{"The api key field is required."}}} } return nil } // baseURLOf is a model's base URL with nil and blank alike. func baseURLOf(m *models.AiModel) string { if m.BaseURL == nil { return "" } return strings.TrimSpace(*m.BaseURL) } // FormBeforeUpdate keeps the stored key when the form sent it back empty // or redacted. A kept key never follows a changed endpoint: when the // adapter or base URL differs from the stored one, the key must be entered // again, so an operator who cannot read the key cannot redirect it to a // host they control either. func (ModelsAdminController) FormBeforeUpdate(ctx context.Context, model any) error { m, ok := model.(*models.AiModel) if !ok { return errors.New("golem: unexpected admin record") } if !keyMissing(m) { return nil } tx, ok := cabana.TxFromContext(ctx) if !ok { return errors.New("golem: no transaction for the model update") } var stored models.AiModel if err := tx.Select("api_key", "adapter", "base_url").Where("id = ?", m.ID).Take(&stored).Error; err != nil { return err } if stored.Adapter != m.Adapter || baseURLOf(&stored) != baseURLOf(m) { return &cabana.ValidationError{Details: map[string]any{"api_key": []string{"Re-enter the API key when changing the adapter or base URL."}}} } m.APIKey = stored.APIKey if keyMissing(m) { return &cabana.ValidationError{Details: map[string]any{"api_key": []string{"The api key field is required."}}} } return nil }