package store import ( "context" "database/sql" "errors" "golang.org/x/crypto/bcrypt" ) func (s *Store) Migrate(adminUsername, adminPassword string) error { stmts := []string{ `create table if not exists site_content ( id integer primary key check (id = 1), hero_title text not null, hero_subtitle text not null, intro_title text not null, intro_text text not null, about_name text not null, about_role text not null, about_bio text not null, email text not null, phone text not null, location text not null, hero_image text not null, about_image text not null )`, `create table if not exists projects ( id integer primary key autoincrement, slug text not null unique, title text not null, location text not null, year text not null, category text not null, description text not null, cover_image text not null, featured integer not null default 0, created_at datetime not null default current_timestamp )`, `create table if not exists project_images ( id integer primary key autoincrement, project_id integer not null references projects(id) on delete cascade, path text not null, caption text not null, position integer not null default 0 )`, `create table if not exists contact_requests ( id integer primary key autoincrement, name text not null, email text not null, message text not null, created_at datetime not null default current_timestamp )`, `create table if not exists admin_users ( id integer primary key autoincrement, username text not null unique, password_hash blob not null )`, `create table if not exists sessions ( token_hash text primary key, user_id integer not null references admin_users(id) on delete cascade, expires_at datetime not null )`, } for _, stmt := range stmts { if _, err := s.db.Exec(stmt); err != nil { return err } } return s.seed(adminUsername, adminPassword) } func (s *Store) seed(adminUsername, adminPassword string) error { var count int if err := s.db.QueryRow(`select count(*) from site_content`).Scan(&count); err != nil { return err } if count == 0 { _, err := s.db.Exec(`insert into site_content ( id, hero_title, hero_subtitle, intro_title, intro_text, about_name, about_role, about_bio, email, phone, location, hero_image, about_image ) values (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, "Archi Folio", "Spatial design, architecture, and interiors shaped through quiet detail.", "Selected residential and cultural spaces", "A compact portfolio for showing image-led architectural work, interior concepts, and project narratives.", "Alex Morgan", "Architect & Interior Designer", "I design calm, functional spaces with attention to proportion, material, light, and the daily rituals of the people who use them.", "studio@example.com", "+44 20 0000 0000", "London, United Kingdom", "/static/placeholders/hero.svg", "/static/placeholders/about.svg", ) if err != nil { return err } } if err := s.db.QueryRow(`select count(*) from projects`).Scan(&count); err != nil { return err } if count == 0 { projects := []Project{ {Slug: "courtyard-house", Title: "Courtyard House", Location: "Bath, UK", Year: "2025", Category: "Residential", Description: "A private house organized around a quiet internal garden, using warm timber, stone, and filtered daylight.", CoverImage: "/static/placeholders/project-1.svg", Featured: true}, {Slug: "atelier-apartment", Title: "Atelier Apartment", Location: "London, UK", Year: "2024", Category: "Interior", Description: "A compact apartment refit with integrated storage, gallery-like surfaces, and a flexible work area.", CoverImage: "/static/placeholders/project-2.svg", Featured: true}, {Slug: "gallery-room", Title: "Gallery Room", Location: "Amsterdam, NL", Year: "2024", Category: "Cultural", Description: "A small exhibition environment designed for shifting light levels, sculpture, and intimate events.", CoverImage: "/static/placeholders/project-3.svg", Featured: true}, } for _, p := range projects { id, err := s.CreateProject(context.Background(), p) if err != nil { return err } for i := 0; i < 3; i++ { if _, err := s.db.Exec(`insert into project_images (project_id, path, caption, position) values (?, ?, ?, ?)`, id, p.CoverImage, p.Title, i); err != nil { return err } } } } hash, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost) if err != nil { return err } var user AdminUser err = s.db.QueryRow(`select id, username, password_hash from admin_users order by id asc limit 1`).Scan(&user.ID, &user.Username, &user.PasswordHash) if errors.Is(err, sql.ErrNoRows) { _, err = s.db.Exec(`insert into admin_users (username, password_hash) values (?, ?)`, adminUsername, hash) return err } if err != nil { return err } if user.Username == adminUsername && bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(adminPassword)) == nil { return nil } _, err = s.db.Exec(`update admin_users set username = ?, password_hash = ? where id = ?`, adminUsername, hash, user.ID) if err != nil { return err } _, err = s.db.Exec(`delete from sessions`) return err }