5.4 KB
setup.go
package controllers
import (
"fmt"
"net/http"
"github.com/readysite/readysite/pkg/application"
"github.com/readysite/readysite/website/internal/access"
"github.com/readysite/readysite/website/internal/helpers"
"github.com/readysite/readysite/website/models"
"golang.org/x/crypto/bcrypt"
)
// Setup returns the setup wizard controller.
func Setup() (string, *SetupController) {
return "setup", &SetupController{}
}
// SetupController handles first-run setup wizard.
type SetupController struct {
application.BaseController
}
// Setup registers routes.
func (c *SetupController) Setup(app *application.App) {
c.BaseController.Setup(app)
// All setup routes should redirect to dashboard if setup is complete
http.Handle("GET /setup", app.Serve("setup/index.html", c.requireSetupIncomplete))
http.Handle("GET /setup/account", app.Serve("setup/account.html", c.requireSetupIncomplete))
http.Handle("POST /setup/account", app.Method(c, "CreateAccount", c.requireSetupIncomplete))
http.Handle("GET /setup/ai", app.Serve("setup/ai.html", c.requireAccount))
http.Handle("POST /setup/ai", app.Method(c, "ConfigureAI", c.requireAccount))
http.Handle("GET /setup/complete", app.Serve("setup/complete.html", c.requireAccount))
http.Handle("POST /setup/complete", app.Method(c, "Complete", c.requireAccount))
}
// Handle implements Controller interface with value receiver for request isolation.
func (c SetupController) Handle(r *http.Request) application.Controller {
c.Request = r
return &c
}
// requireSetupIncomplete redirects to dashboard if setup is already complete.
func (c *SetupController) requireSetupIncomplete(app *application.App, w http.ResponseWriter, r *http.Request) bool {
if !c.NeedsSetup() {
http.Redirect(w, r, "/admin", http.StatusSeeOther)
return false
}
return true
}
// requireAccount redirects to /setup if no admin account exists yet.
func (c *SetupController) requireAccount(app *application.App, w http.ResponseWriter, r *http.Request) bool {
if !c.requireSetupIncomplete(app, w, r) {
return false
}
if models.Users.Count("WHERE Role = ?", access.RoleAdmin) == 0 {
http.Redirect(w, r, "/setup", http.StatusSeeOther)
return false
}
return true
}
// NeedsSetup returns true if the first-run setup hasn't been completed.
func (c *SetupController) NeedsSetup() bool {
// Check if setup has been marked complete
// This allows the multi-step wizard to work (account -> ai -> complete)
return helpers.GetSetting(models.SettingSetupComplete) != "true"
}
// Step returns the current setup step based on the URL path.
func (c *SetupController) Step() string {
if c.Request == nil {
return "account"
}
path := c.URL.Path
switch path {
case "/setup/ai":
return "ai"
case "/setup/complete":
return "complete"
default:
return "account"
}
}
// CreateAccount creates the admin account and signs them in.
func (c *SetupController) CreateAccount(w http.ResponseWriter, r *http.Request) {
// SECURITY: Check again that no admin exists to prevent race condition
// where two concurrent requests both pass the bouncer check
if models.Users.Count("WHERE Role = ?", access.RoleAdmin) > 0 {
c.RenderError(w, r, fmt.Errorf("An admin account already exists"))
return
}
email := r.FormValue("email")
password := r.FormValue("password")
name := r.FormValue("name")
if email == "" || password == "" {
c.RenderError(w, r, fmt.Errorf("Email and password are required"))
return
}
// Check password length
if len(password) < 8 {
c.RenderError(w, r, fmt.Errorf("Password must be at least 8 characters"))
return
}
// Hash password
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
c.RenderError(w, r, fmt.Errorf("Failed to hash password: %w", err))
return
}
user := &models.User{
Email: email,
PasswordHash: string(hash),
Name: name,
Role: access.RoleAdmin,
Verified: true,
}
if _, err := models.Users.Insert(user); err != nil {
// Could be a race - another admin was created
c.RenderError(w, r, fmt.Errorf("Failed to create user: %w", err))
return
}
// Sign in the new user immediately
access.SetSessionCookie(w, r, user.ID)
c.Redirect(w, r, "/setup/ai")
}
// ConfigureAI saves AI configuration.
func (c *SetupController) ConfigureAI(w http.ResponseWriter, r *http.Request) {
provider := r.FormValue("provider")
apiKey := r.FormValue("api_key")
model := r.FormValue("model")
if provider == "" {
provider = "mock" // Default to mock for testing
}
helpers.SetSetting(models.SettingAIProvider, provider)
if apiKey != "" {
helpers.SetSetting(models.SettingAIAPIKey, apiKey)
}
if model != "" {
helpers.SetSetting(models.SettingAIModel, model)
}
c.Redirect(w, r, "/setup/complete")
}
// Complete marks setup as complete and redirects to dashboard.
func (c *SetupController) Complete(w http.ResponseWriter, r *http.Request) {
siteName := r.FormValue("site_name")
siteDescription := r.FormValue("site_description")
if siteName != "" {
helpers.SetSetting(models.SettingSiteName, siteName)
}
if siteDescription != "" {
helpers.SetSetting(models.SettingSiteDescription, siteDescription)
}
helpers.SetSetting(models.SettingSetupComplete, "true")
// Auto sign-in the admin user
admin, err := models.Users.First("WHERE Role = ?", access.RoleAdmin)
if err == nil && admin != nil {
access.SetSessionCookie(w, r, admin.ID)
}
// Full page redirect (not boosted) since layout changes
http.Redirect(w, r, "/admin", http.StatusSeeOther)
}