package main import ( "errors" "fmt" "net/url" "strings" "time" "unicode" "gorm.io/gorm" ) const ( defaultMapTileSourceName = "OpenStreetMap Japan" defaultMapTileSourceURLTemplate = "https://tile.openstreetmap.jp/{z}/{x}/{y}.png" defaultMapTileSourceAttribution = "© OpenStreetMap contributors" defaultMapTileSourceMaxZoom = 19 maxMapTileSourceURLLength = 2048 ) var ( errMapTileSourceAlreadyExists = errors.New("map source already exists") errMapTileSourceCannotDeleteDefault = errors.New("default map source cannot be deleted") errMapTileSourceCannotDisableDefault = errors.New("default map source cannot be disabled") errMapTileSourceDefaultMustBeEnabled = errors.New("default map source must be enabled") ) type mapTileSourceInput struct { Name string URLTemplate string Attribution string MaxZoom int Enabled bool IsDefault bool } func (s *store) ListMapTileSources(opts listOptions) ([]mapTileSourceRecord, error) { opts = normalizeListOptions(opts) var rows []mapTileSourceRecord q := s.db.Model(&mapTileSourceRecord{}). Order("is_default DESC"). Order("updated_at DESC"). Order("id DESC"). Limit(opts.Limit). Offset(opts.Offset) return rows, q.Find(&rows).Error } func (s *store) CountMapTileSources(opts listOptions) (int64, error) { var total int64 return total, s.db.Model(&mapTileSourceRecord{}).Count(&total).Error } func (s *store) ListEnabledMapTileSources() ([]mapTileSourceRecord, error) { var rows []mapTileSourceRecord if err := s.db.Model(&mapTileSourceRecord{}). Where("enabled = ?", true). Order("is_default DESC"). Order("updated_at DESC"). Order("id DESC"). Find(&rows).Error; err != nil { return nil, err } if len(rows) == 0 { return []mapTileSourceRecord{defaultMapTileSourceRecord()}, nil } return rows, nil } func (s *store) GetDefaultMapTileSource() (*mapTileSourceRecord, error) { var row mapTileSourceRecord err := s.db.Where("enabled = ? AND is_default = ?", true, true).Order("id ASC").Take(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { fallback := defaultMapTileSourceRecord() return &fallback, nil } if err != nil { return nil, err } return &row, nil } func (s *store) CreateMapTileSource(input mapTileSourceInput) (*mapTileSourceRecord, error) { row, err := mapTileSourceFromInput(input) if err != nil { return nil, err } if row.IsDefault && !row.Enabled { return nil, errMapTileSourceDefaultMustBeEnabled } if err := s.ensureMapTileSourceUnique(0, row.Name, row.URLTemplate); err != nil { return nil, err } if err := s.db.Transaction(func(tx *gorm.DB) error { if row.IsDefault { if err := tx.Model(&mapTileSourceRecord{}).Where("is_default = ?", true).Update("is_default", false).Error; err != nil { return err } } return tx.Create(row).Error }); err != nil { return nil, err } return row, nil } func (s *store) UpdateMapTileSource(id uint64, input mapTileSourceInput) (*mapTileSourceRecord, error) { if id == 0 { return nil, fmt.Errorf("map source id is required") } row, err := mapTileSourceFromInput(input) if err != nil { return nil, err } var updated mapTileSourceRecord if err := s.db.Transaction(func(tx *gorm.DB) error { var existing mapTileSourceRecord if err := tx.Where("id = ?", id).Take(&existing).Error; err != nil { return err } if existing.IsDefault && !row.Enabled { return errMapTileSourceCannotDisableDefault } if row.IsDefault && !row.Enabled { return errMapTileSourceDefaultMustBeEnabled } if !row.IsDefault && existing.IsDefault { row.IsDefault = true } if err := ensureMapTileSourceUniqueTx(tx, id, row.Name, row.URLTemplate); err != nil { return err } if row.IsDefault { if err := tx.Model(&mapTileSourceRecord{}).Where("id <> ? AND is_default = ?", id, true).Update("is_default", false).Error; err != nil { return err } } updates := map[string]any{ "name": row.Name, "url_template": row.URLTemplate, "attribution": row.Attribution, "max_zoom": row.MaxZoom, "enabled": row.Enabled, "is_default": row.IsDefault, "updated_at": time.Now(), } if err := tx.Model(&mapTileSourceRecord{}).Where("id = ?", id).Updates(updates).Error; err != nil { return err } return tx.Where("id = ?", id).Take(&updated).Error }); err != nil { return nil, err } return &updated, nil } func (s *store) DeleteMapTileSource(id uint64) error { if id == 0 { return fmt.Errorf("map source id is required") } return s.db.Transaction(func(tx *gorm.DB) error { var row mapTileSourceRecord if err := tx.Where("id = ?", id).Take(&row).Error; err != nil { return err } if row.IsDefault { return errMapTileSourceCannotDeleteDefault } result := tx.Where("id = ?", id).Delete(&mapTileSourceRecord{}) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return gorm.ErrRecordNotFound } return nil }) } func (s *store) SetDefaultMapTileSource(id uint64) (*mapTileSourceRecord, error) { if id == 0 { return nil, fmt.Errorf("map source id is required") } var row mapTileSourceRecord if err := s.db.Transaction(func(tx *gorm.DB) error { if err := tx.Where("id = ?", id).Take(&row).Error; err != nil { return err } if !row.Enabled { return errMapTileSourceDefaultMustBeEnabled } if err := tx.Model(&mapTileSourceRecord{}).Where("is_default = ?", true).Update("is_default", false).Error; err != nil { return err } if err := tx.Model(&mapTileSourceRecord{}).Where("id = ?", id).Updates(map[string]any{"is_default": true, "updated_at": time.Now()}).Error; err != nil { return err } return tx.Where("id = ?", id).Take(&row).Error }); err != nil { return nil, err } return &row, nil } func (s *store) EnsureDefaultMapTileSource() error { return s.db.Transaction(func(tx *gorm.DB) error { var count int64 if err := tx.Model(&mapTileSourceRecord{}).Count(&count).Error; err != nil { return err } if count == 0 { row := defaultMapTileSourceRecord() return tx.Create(&row).Error } var defaults []mapTileSourceRecord if err := tx.Where("enabled = ? AND is_default = ?", true, true).Order("id ASC").Find(&defaults).Error; err != nil { return err } if len(defaults) > 0 { return tx.Model(&mapTileSourceRecord{}).Where("id <> ? AND is_default = ?", defaults[0].ID, true).Update("is_default", false).Error } var enabled mapTileSourceRecord err := tx.Where("enabled = ?", true).Order("id ASC").Take(&enabled).Error if err == nil { return tx.Model(&mapTileSourceRecord{}).Where("id = ?", enabled.ID).Updates(map[string]any{"is_default": true, "updated_at": time.Now()}).Error } if !errors.Is(err, gorm.ErrRecordNotFound) { return err } row := defaultMapTileSourceRecord() var existing mapTileSourceRecord err = tx.Where("name = ? OR url_template = ?", row.Name, row.URLTemplate).Order("id ASC").Take(&existing).Error if err == nil { return tx.Model(&mapTileSourceRecord{}).Where("id = ?", existing.ID).Updates(map[string]any{"enabled": true, "is_default": true, "updated_at": time.Now()}).Error } if !errors.Is(err, gorm.ErrRecordNotFound) { return err } return tx.Create(&row).Error }) } func defaultMapTileSourceRecord() mapTileSourceRecord { return mapTileSourceRecord{ Name: defaultMapTileSourceName, URLTemplate: defaultMapTileSourceURLTemplate, Attribution: defaultMapTileSourceAttribution, MaxZoom: defaultMapTileSourceMaxZoom, Enabled: true, IsDefault: true, } } func mapTileSourceFromInput(input mapTileSourceInput) (*mapTileSourceRecord, error) { name := strings.TrimSpace(input.Name) if name == "" { return nil, fmt.Errorf("map source name is required") } urlTemplate, err := normalizeMapTileSourceURLTemplate(input.URLTemplate) if err != nil { return nil, err } maxZoom := input.MaxZoom if maxZoom == 0 { maxZoom = defaultMapTileSourceMaxZoom } if maxZoom < 1 || maxZoom > 30 { return nil, fmt.Errorf("max zoom must be between 1 and 30") } return &mapTileSourceRecord{ Name: name, URLTemplate: urlTemplate, Attribution: strings.TrimSpace(input.Attribution), MaxZoom: maxZoom, Enabled: input.Enabled, IsDefault: input.IsDefault, }, nil } func normalizeMapTileSourceURLTemplate(value string) (string, error) { value = strings.TrimSpace(value) if value == "" { return "", fmt.Errorf("map source url template is required") } if len(value) > maxMapTileSourceURLLength { return "", fmt.Errorf("map source url template is too long") } for _, r := range value { if unicode.IsControl(r) || unicode.IsSpace(r) { return "", fmt.Errorf("map source url template must not contain whitespace or control characters") } } for _, placeholder := range []string{"{z}", "{x}", "{y}"} { if strings.Count(value, placeholder) != 1 { return "", fmt.Errorf("map source url template must contain %s exactly once", placeholder) } } parsed, err := url.Parse(value) if err != nil { return "", fmt.Errorf("map source url template is invalid") } if parsed.Scheme != "http" && parsed.Scheme != "https" { return "", fmt.Errorf("map source url template must use http or https") } if parsed.Host == "" { return "", fmt.Errorf("map source url template host is required") } if parsed.User != nil { return "", fmt.Errorf("map source url template must not contain credentials") } return value, nil } func (s *store) ensureMapTileSourceUnique(id uint64, name, urlTemplate string) error { return ensureMapTileSourceUniqueTx(s.db, id, name, urlTemplate) } func ensureMapTileSourceUniqueTx(tx *gorm.DB, id uint64, name, urlTemplate string) error { var existing mapTileSourceRecord q := tx.Where("name = ? OR url_template = ?", name, urlTemplate) if id != 0 { q = q.Where("id <> ?", id) } err := q.Take(&existing).Error if err == nil { return errMapTileSourceAlreadyExists } if errors.Is(err, gorm.ErrRecordNotFound) { return nil } return err }