package app import ( "errors" "fmt" "os" "time" "github.com/h44z/wg-portal/internal/adapters" "github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/domain" "github.com/sirupsen/logrus" "gorm.io/gorm" ) func migrateFromV1(cfg *config.Config, db *gorm.DB, source, typ string) error { sourceType := config.SupportedDatabase(typ) switch sourceType { case config.DatabaseMySQL, config.DatabasePostgres, config.DatabaseMsSQL: case config.DatabaseSQLite: if _, err := os.Stat(source); os.IsNotExist(err) { return fmt.Errorf("invalid source database: %w", err) } default: return errors.New("unsupported database") } oldDb, err := adapters.NewDatabase(config.DatabaseConfig{ Type: sourceType, DSN: source, }) if err != nil { return fmt.Errorf("failed to open old database: %w", err) } // check if old db is a valid WireGuard Portal v1 database type DatabaseMigrationInfo struct { Version string `gorm:"primaryKey"` Applied time.Time } lastVersion := DatabaseMigrationInfo{} err = oldDb.Order("applied desc, version desc").FirstOrInit(&lastVersion).Error if err != nil { return fmt.Errorf("unable to validate old database: %w", err) } latestVersion := "1.0.9" if lastVersion.Version != latestVersion { return fmt.Errorf("unsupported old version, update to database version %s first: %w", latestVersion, err) } logrus.Infof("Found valid V1 database with version: %s", lastVersion.Version) if err := migrateV1Users(oldDb, db); err != nil { return fmt.Errorf("user migration failed: %w", err) } if err := migrateV1Interfaces(oldDb, db); err != nil { return fmt.Errorf("user migration failed: %w", err) } if err := migrateV1Peers(oldDb, db); err != nil { return fmt.Errorf("peer migration failed: %w", err) } logrus.Infof("Migrated V1 database with version %s, please restart WireGuard Portal", lastVersion.Version) return nil } func migrateV1Users(oldDb, newDb *gorm.DB) error { type User struct { Email string `gorm:"primaryKey"` Source string IsAdmin bool Firstname string Lastname string Phone string Password string CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` } var oldUsers []User err := oldDb.Find(&oldUsers).Error if err != nil { return fmt.Errorf("unable to fetch old user records: %w", err) } for _, oldUser := range oldUsers { var deletionTime *time.Time deletionReason := "" if oldUser.DeletedAt.Valid { delTime := oldUser.DeletedAt.Time deletionTime = &delTime deletionReason = "disabled prior to migration" } newUser := domain.User{ BaseModel: domain.BaseModel{ CreatedBy: "v1migrator", UpdatedBy: "v1migrator", CreatedAt: oldUser.CreatedAt, UpdatedAt: oldUser.UpdatedAt, }, Identifier: domain.UserIdentifier(oldUser.Email), Email: oldUser.Email, Source: domain.UserSource(oldUser.Source), ProviderName: "", IsAdmin: oldUser.IsAdmin, Firstname: oldUser.Firstname, Lastname: oldUser.Lastname, Phone: oldUser.Phone, Department: "", Notes: "", Password: domain.PrivateString(oldUser.Password), Disabled: deletionTime, DisabledReason: deletionReason, Locked: nil, LockedReason: "", LinkedPeerCount: 0, } if err := newDb.Save(&newUser).Error; err != nil { return fmt.Errorf("failed to migrate user %s: %w", oldUser.Email, err) } logrus.Debugf(" - User %s migrated", newUser.Identifier) } return nil } func migrateV1Interfaces(oldDb, newDb *gorm.DB) error { type Device struct { Type string DeviceName string `gorm:"primaryKey"` DisplayName string PrivateKey string ListenPort int FirewallMark uint32 PublicKey string Mtu int IPsStr string DNSStr string RoutingTable string PreUp string PostUp string PreDown string PostDown string SaveConfig bool DefaultEndpoint string DefaultAllowedIPsStr string DefaultPersistentKeepalive int CreatedAt time.Time UpdatedAt time.Time } var oldDevices []Device err := oldDb.Find(&oldDevices).Error if err != nil { return fmt.Errorf("unable to fetch old device records: %w", err) } for _, oldDevice := range oldDevices { ips, err := domain.CidrsFromString(oldDevice.IPsStr) if err != nil { return fmt.Errorf("failed to parse %s ip addresses: %w", oldDevice.DeviceName, err) } networks := make([]domain.Cidr, len(ips)) for i, ip := range ips { networks[i] = domain.CidrFromIpNet(*ip.IpNet()) } newInterface := domain.Interface{ BaseModel: domain.BaseModel{ CreatedBy: "v1migrator", UpdatedBy: "v1migrator", CreatedAt: oldDevice.CreatedAt, UpdatedAt: oldDevice.UpdatedAt, }, Identifier: domain.InterfaceIdentifier(oldDevice.DeviceName), KeyPair: domain.KeyPair{ PrivateKey: oldDevice.PrivateKey, PublicKey: oldDevice.PublicKey, }, ListenPort: oldDevice.ListenPort, Addresses: ips, DnsStr: "", DnsSearchStr: "", Mtu: oldDevice.Mtu, FirewallMark: oldDevice.FirewallMark, RoutingTable: oldDevice.RoutingTable, PreUp: oldDevice.PreUp, PostUp: oldDevice.PostUp, PreDown: oldDevice.PreDown, PostDown: oldDevice.PostDown, SaveConfig: oldDevice.SaveConfig, DisplayName: oldDevice.DisplayName, Type: domain.InterfaceType(oldDevice.Type), DriverType: "", Disabled: nil, DisabledReason: "", PeerDefNetworkStr: domain.CidrsToString(networks), PeerDefDnsStr: oldDevice.DNSStr, PeerDefDnsSearchStr: "", PeerDefEndpoint: oldDevice.DefaultEndpoint, PeerDefAllowedIPsStr: oldDevice.DefaultAllowedIPsStr, PeerDefMtu: oldDevice.Mtu, PeerDefPersistentKeepalive: oldDevice.DefaultPersistentKeepalive, PeerDefFirewallMark: 0, PeerDefRoutingTable: "", PeerDefPreUp: "", PeerDefPostUp: "", PeerDefPreDown: "", PeerDefPostDown: "", } if err := newDb.Save(&newInterface).Error; err != nil { return fmt.Errorf("failed to migrate device %s: %w", oldDevice.DeviceName, err) } logrus.Debugf(" - Interface %s migrated", newInterface.Identifier) } return nil } func migrateV1Peers(oldDb, newDb *gorm.DB) error { type Peer struct { UID string DeviceName string `gorm:"index"` Identifier string Email string `gorm:"index" form:"mail" binding:"required,email"` IgnoreGlobalSettings bool PublicKey string `gorm:"primaryKey"` PresharedKey string AllowedIPsStr string AllowedIPsSrvStr string Endpoint string PersistentKeepalive int PrivateKey string IPsStr string DNSStr string Mtu int DeactivatedAt *time.Time `json:",omitempty"` DeactivatedReason string `json:",omitempty"` ExpiresAt *time.Time CreatedBy string UpdatedBy string CreatedAt time.Time UpdatedAt time.Time } var oldPeers []Peer err := oldDb.Find(&oldPeers).Error if err != nil { return fmt.Errorf("unable to fetch old peer records: %w", err) } for _, oldPeer := range oldPeers { ips, err := domain.CidrsFromString(oldPeer.IPsStr) if err != nil { return fmt.Errorf("failed to parse %s ip addresses: %w", oldPeer.PublicKey, err) } var disableTime *time.Time disableReason := "" if oldPeer.DeactivatedAt != nil { disTime := *oldPeer.DeactivatedAt disableTime = &disTime disableReason = oldPeer.DeactivatedReason } var expiryTime *time.Time if oldPeer.ExpiresAt != nil { expTime := *oldPeer.ExpiresAt expiryTime = &expTime } var iface domain.Interface var ifaceType domain.InterfaceType err = newDb.First(&iface, "identifier = ?", oldPeer.DeviceName).Error if err != nil { return fmt.Errorf("failed to find interface %s for peer %s: %w", oldPeer.DeviceName, oldPeer.PublicKey, err) } switch iface.Type { case domain.InterfaceTypeClient: ifaceType = domain.InterfaceTypeServer case domain.InterfaceTypeServer: ifaceType = domain.InterfaceTypeClient case domain.InterfaceTypeAny: ifaceType = domain.InterfaceTypeAny } var user domain.User err = newDb.First(&user, "identifier = ?", oldPeer.Email).Error // migrated users use the email address as identifier if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("failed to find user %s for peer %s: %w", oldPeer.Email, oldPeer.PublicKey, err) } if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { // create dummy user now := time.Now() user = domain.User{ BaseModel: domain.BaseModel{ CreatedBy: "v1migrator", UpdatedBy: "v1migrator", CreatedAt: now, UpdatedAt: now, }, Identifier: domain.UserIdentifier(oldPeer.Email), Email: oldPeer.Email, Source: domain.UserSourceDatabase, ProviderName: "", IsAdmin: false, Locked: &now, LockedReason: domain.DisabledReasonMigrationDummy, Notes: "created by migration from v1", } if err := newDb.Save(&user).Error; err != nil { return fmt.Errorf("failed to migrate dummy user %s: %w", oldPeer.Email, err) } logrus.Debugf(" - Dummy User %s migrated", user.Identifier) } newPeer := domain.Peer{ BaseModel: domain.BaseModel{ CreatedBy: "v1migrator", UpdatedBy: "v1migrator", CreatedAt: oldPeer.CreatedAt, UpdatedAt: oldPeer.UpdatedAt, }, Endpoint: domain.NewConfigOption(oldPeer.Endpoint, !oldPeer.IgnoreGlobalSettings), EndpointPublicKey: domain.NewConfigOption(iface.PublicKey, !oldPeer.IgnoreGlobalSettings), AllowedIPsStr: domain.NewConfigOption(oldPeer.AllowedIPsStr, !oldPeer.IgnoreGlobalSettings), ExtraAllowedIPsStr: oldPeer.AllowedIPsSrvStr, PresharedKey: domain.PreSharedKey(oldPeer.PresharedKey), PersistentKeepalive: domain.NewConfigOption(oldPeer.PersistentKeepalive, !oldPeer.IgnoreGlobalSettings), DisplayName: oldPeer.Identifier, Identifier: domain.PeerIdentifier(oldPeer.PublicKey), UserIdentifier: user.Identifier, InterfaceIdentifier: iface.Identifier, Disabled: disableTime, DisabledReason: disableReason, ExpiresAt: expiryTime, Notes: "", Interface: domain.PeerInterfaceConfig{ KeyPair: domain.KeyPair{ PrivateKey: oldPeer.PrivateKey, PublicKey: oldPeer.PublicKey, }, Type: ifaceType, Addresses: ips, DnsStr: domain.NewConfigOption(oldPeer.DNSStr, !oldPeer.IgnoreGlobalSettings), DnsSearchStr: domain.NewConfigOption(iface.PeerDefDnsSearchStr, !oldPeer.IgnoreGlobalSettings), Mtu: domain.NewConfigOption(oldPeer.Mtu, !oldPeer.IgnoreGlobalSettings), FirewallMark: domain.NewConfigOption(iface.PeerDefFirewallMark, !oldPeer.IgnoreGlobalSettings), RoutingTable: domain.NewConfigOption(iface.PeerDefRoutingTable, !oldPeer.IgnoreGlobalSettings), PreUp: domain.NewConfigOption(iface.PeerDefPreUp, !oldPeer.IgnoreGlobalSettings), PostUp: domain.NewConfigOption(iface.PeerDefPostUp, !oldPeer.IgnoreGlobalSettings), PreDown: domain.NewConfigOption(iface.PeerDefPreDown, !oldPeer.IgnoreGlobalSettings), PostDown: domain.NewConfigOption(iface.PeerDefPostDown, !oldPeer.IgnoreGlobalSettings), }, } if err := newDb.Save(&newPeer).Error; err != nil { return fmt.Errorf("failed to migrate peer %s (%s): %w", oldPeer.Identifier, oldPeer.PublicKey, err) } logrus.Debugf(" - Peer %s migrated", newPeer.Identifier) } return nil }