• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DigitalTolk / wireguard-ui / 24791061837

22 Apr 2026 04:50PM UTC coverage: 81.067%. First build
24791061837

push

github

web-flow
Add auditlogs (#1)

* Add auditlogs

* test

* test

* test

* test

* test

* test

164 of 233 branches covered (70.39%)

Branch coverage included in aggregate %.

1878 of 2390 new or added lines in 34 files covered. (78.58%)

2722 of 3327 relevant lines covered (81.82%)

6.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.43
/store/sqlitedb/sqlitedb.go
1
package sqlitedb
2

3
import (
4
        "database/sql"
5
        "embed"
6
        "encoding/base64"
7
        "encoding/json"
8
        "fmt"
9
        "net"
10
        "os"
11
        "path/filepath"
12
        "strconv"
13
        "time"
14

15
        _ "modernc.org/sqlite"
16

17
        "github.com/skip2/go-qrcode"
18
        "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
19

20
        "github.com/DigitalTolk/wireguard-ui/model"
21
        "github.com/DigitalTolk/wireguard-ui/util"
22
)
23

24
const (
25
        userColumns         = "username, email, display_name, COALESCE(oidc_sub, ''), admin, created_at, updated_at"
26
        qrCodeDataURIPrefix = "data:image/png;base64,"
27
)
28

29
//go:embed schema.sql
30
var schemaFS embed.FS
31

32
// SqliteDB implements store.IStore using SQLite
33
type SqliteDB struct {
34
        db     *sql.DB
35
        dbPath string
36
}
37

38
// New creates a new SqliteDB instance
39
func New(dbPath string) (*SqliteDB, error) {
60✔
40
        // ensure parent directory exists
60✔
41
        dir := filepath.Dir(dbPath)
60✔
42
        if err := os.MkdirAll(dir, 0750); err != nil {
60✔
NEW
43
                return nil, fmt.Errorf("cannot create database directory: %w", err)
×
NEW
44
        }
×
45

46
        db, err := sql.Open("sqlite", dbPath+"?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=ON")
60✔
47
        if err != nil {
60✔
NEW
48
                return nil, fmt.Errorf("cannot open database: %w", err)
×
NEW
49
        }
×
50

51
        // apply schema
52
        schema, err := schemaFS.ReadFile("schema.sql")
60✔
53
        if err != nil {
60✔
NEW
54
                return nil, fmt.Errorf("cannot read schema: %w", err)
×
NEW
55
        }
×
56
        if _, err := db.Exec(string(schema)); err != nil {
60✔
NEW
57
                return nil, fmt.Errorf("cannot apply schema: %w", err)
×
NEW
58
        }
×
59

60
        return &SqliteDB{db: db, dbPath: dbPath}, nil
60✔
61
}
62

63
// Init initializes the database with default values if they don't exist
64
func (o *SqliteDB) Init() error {
31✔
65
        // server interface
31✔
66
        var ifaceCount int
31✔
67
        o.db.QueryRow("SELECT COUNT(*) FROM server_interface").Scan(&ifaceCount)
31✔
68
        if ifaceCount == 0 {
61✔
69
                addresses := util.LookupEnvOrStrings(util.ServerAddressesEnvVar, []string{util.DefaultServerAddress})
30✔
70
                listenPort := util.LookupEnvOrInt(util.ServerListenPortEnvVar, util.DefaultServerPort)
30✔
71
                postUp := util.LookupEnvOrString(util.ServerPostUpScriptEnvVar, "")
30✔
72
                postDown := util.LookupEnvOrString(util.ServerPostDownScriptEnvVar, "")
30✔
73
                addrJSON, _ := json.Marshal(addresses)
30✔
74
                _, err := o.db.Exec(
30✔
75
                        `INSERT INTO server_interface (id, addresses, listen_port, post_up, post_down, updated_at) VALUES (1, ?, ?, ?, ?, ?)`,
30✔
76
                        string(addrJSON), listenPort, postUp, postDown, time.Now().UTC(),
30✔
77
                )
30✔
78
                if err != nil {
30✔
NEW
79
                        return fmt.Errorf("cannot init server interface: %w", err)
×
NEW
80
                }
×
81
        }
82

83
        // server keypair
84
        var kpCount int
31✔
85
        o.db.QueryRow("SELECT COUNT(*) FROM server_keypair").Scan(&kpCount)
31✔
86
        if kpCount == 0 {
61✔
87
                key, err := wgtypes.GeneratePrivateKey()
30✔
88
                if err != nil {
30✔
NEW
89
                        return fmt.Errorf("cannot generate server keypair: %w", err)
×
NEW
90
                }
×
91
                _, err = o.db.Exec(
30✔
92
                        `INSERT INTO server_keypair (id, private_key, public_key, updated_at) VALUES (1, ?, ?, ?)`,
30✔
93
                        key.String(), key.PublicKey().String(), time.Now().UTC(),
30✔
94
                )
30✔
95
                if err != nil {
30✔
NEW
96
                        return fmt.Errorf("cannot init server keypair: %w", err)
×
NEW
97
                }
×
98
        }
99

100
        // global settings
101
        var gsCount int
31✔
102
        o.db.QueryRow("SELECT COUNT(*) FROM global_settings").Scan(&gsCount)
31✔
103
        if gsCount == 0 {
61✔
104
                endpointAddress := util.LookupEnvOrString(util.EndpointAddressEnvVar, "")
30✔
105
                if endpointAddress == "" {
30✔
NEW
106
                        publicInterface, err := util.GetPublicIP()
×
NEW
107
                        if err != nil {
×
NEW
108
                                return fmt.Errorf("cannot detect public IP: %w", err)
×
NEW
109
                        }
×
NEW
110
                        endpointAddress = publicInterface.IPAddress
×
111
                }
112
                dnsServers := util.LookupEnvOrStrings(util.DNSEnvVar, []string{util.DefaultDNS})
30✔
113
                dnsJSON, _ := json.Marshal(dnsServers)
30✔
114
                _, err := o.db.Exec(
30✔
115
                        `INSERT INTO global_settings (id, endpoint_address, dns_servers, mtu, persistent_keepalive, firewall_mark, "table", config_file_path, updated_at)
30✔
116
                         VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)`,
30✔
117
                        endpointAddress,
30✔
118
                        string(dnsJSON),
30✔
119
                        util.LookupEnvOrInt(util.MTUEnvVar, util.DefaultMTU),
30✔
120
                        util.LookupEnvOrInt(util.PersistentKeepaliveEnvVar, util.DefaultPersistentKeepalive),
30✔
121
                        util.LookupEnvOrString(util.FirewallMarkEnvVar, util.DefaultFirewallMark),
30✔
122
                        util.LookupEnvOrString(util.TableEnvVar, util.DefaultTable),
30✔
123
                        util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath),
30✔
124
                        time.Now().UTC(),
30✔
125
                )
30✔
126
                if err != nil {
30✔
NEW
127
                        return fmt.Errorf("cannot init global settings: %w", err)
×
NEW
128
                }
×
129
        }
130

131
        // hashes
132
        var hashCount int
31✔
133
        o.db.QueryRow("SELECT COUNT(*) FROM hashes").Scan(&hashCount)
31✔
134
        if hashCount == 0 {
61✔
135
                o.db.Exec(`INSERT INTO hashes (id, client, server) VALUES (1, 'none', 'none')`)
30✔
136
        }
30✔
137

138
        // default user
139
        var userCount int
31✔
140
        o.db.QueryRow("SELECT COUNT(*) FROM users").Scan(&userCount)
31✔
141
        if userCount == 0 {
61✔
142
                username := util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername)
30✔
143
                now := time.Now().UTC()
30✔
144

30✔
145
                _, err := o.db.Exec(
30✔
146
                        `INSERT INTO users (username, admin, created_at, updated_at) VALUES (?, ?, ?, ?)`,
30✔
147
                        username, true, now, now,
30✔
148
                )
30✔
149
                if err != nil {
30✔
NEW
150
                        return fmt.Errorf("cannot create default user: %w", err)
×
NEW
151
                }
×
152
        }
153

154
        // init caches
155
        users, err := o.GetUsers()
31✔
156
        if err == nil {
62✔
157
                util.DBUsersToCRC32Mutex.Lock()
31✔
158
                for _, user := range users {
62✔
159
                        util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
31✔
160
                }
31✔
161
                util.DBUsersToCRC32Mutex.Unlock()
31✔
162
        }
163

164
        clients, err := o.GetClients(false)
31✔
165
        if err == nil {
62✔
166
                for _, cl := range clients {
31✔
NEW
167
                        client := cl.Client
×
NEW
168
                        if client.Enabled && len(client.TgUserid) > 0 {
×
NEW
169
                                if userid, err := strconv.ParseInt(client.TgUserid, 10, 64); err == nil {
×
NEW
170
                                        util.UpdateTgToClientID(userid, client.ID)
×
NEW
171
                                }
×
172
                        }
173
                }
174
        }
175

176
        return nil
31✔
177
}
178

179
// GetUsers returns all users
180
func (o *SqliteDB) GetUsers() ([]model.User, error) {
33✔
181
        rows, err := o.db.Query("SELECT " + userColumns + " FROM users")
33✔
182
        if err != nil {
33✔
NEW
183
                return nil, err
×
NEW
184
        }
×
185
        defer rows.Close()
33✔
186

33✔
187
        var users []model.User
33✔
188
        for rows.Next() {
67✔
189
                var u model.User
34✔
190
                if err := rows.Scan(&u.Username, &u.Email, &u.DisplayName, &u.OIDCSub, &u.Admin, &u.CreatedAt, &u.UpdatedAt); err != nil {
34✔
NEW
191
                        return nil, err
×
NEW
192
                }
×
193
                users = append(users, u)
34✔
194
        }
195
        return users, rows.Err()
33✔
196
}
197

198
// GetUserByName returns a single user by username
199
func (o *SqliteDB) GetUserByName(username string) (model.User, error) {
7✔
200
        var u model.User
7✔
201
        err := o.db.QueryRow(
7✔
202
                "SELECT "+userColumns+" FROM users WHERE username = ?",
7✔
203
                username,
7✔
204
        ).Scan(&u.Username, &u.Email, &u.DisplayName, &u.OIDCSub, &u.Admin, &u.CreatedAt, &u.UpdatedAt)
7✔
205
        if err != nil {
9✔
206
                return u, err
2✔
207
        }
2✔
208
        return u, nil
5✔
209
}
210

211
// SaveUser creates or updates a user
212
func (o *SqliteDB) SaveUser(user model.User) error {
10✔
213
        now := time.Now().UTC()
10✔
214
        if user.UpdatedAt.IsZero() {
11✔
215
                user.UpdatedAt = now
1✔
216
        }
1✔
217
        if user.CreatedAt.IsZero() {
11✔
218
                user.CreatedAt = now
1✔
219
        }
1✔
220

221
        _, err := o.db.Exec(
10✔
222
                `INSERT INTO users (username, email, display_name, oidc_sub, admin, created_at, updated_at)
10✔
223
                 VALUES (?, ?, ?, NULLIF(?, ''), ?, ?, ?)
10✔
224
                 ON CONFLICT(username) DO UPDATE SET
10✔
225
                   email = excluded.email,
10✔
226
                   display_name = excluded.display_name,
10✔
227
                   oidc_sub = excluded.oidc_sub,
10✔
228
                   admin = excluded.admin,
10✔
229
                   updated_at = excluded.updated_at`,
10✔
230
                user.Username, user.Email, user.DisplayName, user.OIDCSub, user.Admin, user.CreatedAt, user.UpdatedAt,
10✔
231
        )
10✔
232
        if err != nil {
10✔
NEW
233
                return err
×
NEW
234
        }
×
235
        util.DBUsersToCRC32Mutex.Lock()
10✔
236
        util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
10✔
237
        util.DBUsersToCRC32Mutex.Unlock()
10✔
238
        return nil
10✔
239
}
240

241
// DeleteUser removes a user by username
242
func (o *SqliteDB) DeleteUser(username string) error {
1✔
243
        util.DBUsersToCRC32Mutex.Lock()
1✔
244
        delete(util.DBUsersToCRC32, username)
1✔
245
        util.DBUsersToCRC32Mutex.Unlock()
1✔
246
        _, err := o.db.Exec("DELETE FROM users WHERE username = ?", username)
1✔
247
        return err
1✔
248
}
1✔
249

250
// GetGlobalSettings returns global WireGuard settings
251
func (o *SqliteDB) GetGlobalSettings() (model.GlobalSetting, error) {
16✔
252
        var gs model.GlobalSetting
16✔
253
        var dnsJSON string
16✔
254
        err := o.db.QueryRow(
16✔
255
                `SELECT endpoint_address, dns_servers, mtu, persistent_keepalive, firewall_mark, "table", config_file_path, updated_at
16✔
256
                 FROM global_settings WHERE id = 1`,
16✔
257
        ).Scan(&gs.EndpointAddress, &dnsJSON, &gs.MTU, &gs.PersistentKeepalive, &gs.FirewallMark, &gs.Table, &gs.ConfigFilePath, &gs.UpdatedAt)
16✔
258
        if err != nil {
16✔
NEW
259
                return gs, err
×
NEW
260
        }
×
261
        json.Unmarshal([]byte(dnsJSON), &gs.DNSServers)
16✔
262
        return gs, nil
16✔
263
}
264

265
// GetServer returns the server config (interface + keypair)
266
func (o *SqliteDB) GetServer() (model.Server, error) {
19✔
267
        server := model.Server{}
19✔
268

19✔
269
        // interface
19✔
270
        iface := model.ServerInterface{}
19✔
271
        var addrJSON string
19✔
272
        err := o.db.QueryRow(
19✔
273
                "SELECT addresses, listen_port, post_up, pre_down, post_down, updated_at FROM server_interface WHERE id = 1",
19✔
274
        ).Scan(&addrJSON, &iface.ListenPort, &iface.PostUp, &iface.PreDown, &iface.PostDown, &iface.UpdatedAt)
19✔
275
        if err != nil {
19✔
NEW
276
                return server, fmt.Errorf("cannot read server interface: %w", err)
×
NEW
277
        }
×
278
        json.Unmarshal([]byte(addrJSON), &iface.Addresses)
19✔
279
        server.Interface = &iface
19✔
280

19✔
281
        // keypair
19✔
282
        kp := model.ServerKeypair{}
19✔
283
        err = o.db.QueryRow(
19✔
284
                "SELECT private_key, public_key, updated_at FROM server_keypair WHERE id = 1",
19✔
285
        ).Scan(&kp.PrivateKey, &kp.PublicKey, &kp.UpdatedAt)
19✔
286
        if err != nil {
19✔
NEW
287
                return server, fmt.Errorf("cannot read server keypair: %w", err)
×
NEW
288
        }
×
289
        server.KeyPair = &kp
19✔
290

19✔
291
        return server, nil
19✔
292
}
293

294
// GetClients returns all clients, optionally with QR codes
295
func (o *SqliteDB) GetClients(hasQRCode bool) ([]model.ClientData, error) {
42✔
296
        rows, err := o.db.Query(
42✔
297
                `SELECT id, private_key, public_key, preshared_key, name, email, telegram_userid,
42✔
298
                        subnet_ranges, allocated_ips, allowed_ips, extra_allowed_ips,
42✔
299
                        endpoint, additional_notes, use_server_dns, enabled, created_at, updated_at
42✔
300
                 FROM clients`,
42✔
301
        )
42✔
302
        if err != nil {
42✔
NEW
303
                return nil, err
×
NEW
304
        }
×
305
        defer rows.Close()
42✔
306

42✔
307
        // fetch server/settings once outside the loop for QR generation
42✔
308
        var server model.Server
42✔
309
        var globalSettings model.GlobalSetting
42✔
310
        if hasQRCode {
44✔
311
                server, _ = o.GetServer()
2✔
312
                globalSettings, _ = o.GetGlobalSettings()
2✔
313
        }
2✔
314

315
        var clients []model.ClientData
42✔
316
        for rows.Next() {
49✔
317
                client, err := scanClientFrom(rows)
7✔
318
                if err != nil {
7✔
NEW
319
                        return nil, err
×
NEW
320
                }
×
321

322
                clientData := model.ClientData{Client: &client}
7✔
323

7✔
324
                if hasQRCode && client.PrivateKey != "" {
8✔
325
                        png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256)
1✔
326
                        if err == nil {
2✔
327
                                clientData.QRCode = qrCodeDataURIPrefix + base64.StdEncoding.EncodeToString(png)
1✔
328
                        }
1✔
329
                }
330

331
                clients = append(clients, clientData)
7✔
332
        }
333
        return clients, rows.Err()
42✔
334
}
335

336
// GetClientByID returns a single client by ID
337
func (o *SqliteDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSettings) (model.ClientData, error) {
10✔
338
        clientData := model.ClientData{}
10✔
339

10✔
340
        row := o.db.QueryRow(
10✔
341
                `SELECT id, private_key, public_key, preshared_key, name, email, telegram_userid,
10✔
342
                        subnet_ranges, allocated_ips, allowed_ips, extra_allowed_ips,
10✔
343
                        endpoint, additional_notes, use_server_dns, enabled, created_at, updated_at
10✔
344
                 FROM clients WHERE id = ?`, clientID,
10✔
345
        )
10✔
346

10✔
347
        client, err := scanClientFrom(row)
10✔
348
        if err != nil {
12✔
349
                return clientData, err
2✔
350
        }
2✔
351

352
        if qrCodeSettings.Enabled && client.PrivateKey != "" {
10✔
353
                server, _ := o.GetServer()
2✔
354
                globalSettings, _ := o.GetGlobalSettings()
2✔
355
                if !qrCodeSettings.IncludeDNS {
3✔
356
                        globalSettings.DNSServers = []string{}
1✔
357
                }
1✔
358
                if !qrCodeSettings.IncludeMTU {
3✔
359
                        globalSettings.MTU = 0
1✔
360
                }
1✔
361
                png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256)
2✔
362
                if err == nil {
4✔
363
                        clientData.QRCode = qrCodeDataURIPrefix + base64.StdEncoding.EncodeToString(png)
2✔
364
                }
2✔
365
        }
366

367
        clientData.Client = &client
8✔
368
        return clientData, nil
8✔
369
}
370

371
// SaveClient creates or updates a client
372
func (o *SqliteDB) SaveClient(client model.Client) error {
22✔
373
        subnetJSON, _ := json.Marshal(client.SubnetRanges)
22✔
374
        allocJSON, _ := json.Marshal(client.AllocatedIPs)
22✔
375
        allowJSON, _ := json.Marshal(client.AllowedIPs)
22✔
376
        extraJSON, _ := json.Marshal(client.ExtraAllowedIPs)
22✔
377

22✔
378
        _, err := o.db.Exec(
22✔
379
                `INSERT INTO clients (id, private_key, public_key, preshared_key, name, email, telegram_userid,
22✔
380
                                      subnet_ranges, allocated_ips, allowed_ips, extra_allowed_ips,
22✔
381
                                      endpoint, additional_notes, use_server_dns, enabled, created_at, updated_at)
22✔
382
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
22✔
383
                 ON CONFLICT(id) DO UPDATE SET
22✔
384
                   private_key = excluded.private_key,
22✔
385
                   public_key = excluded.public_key,
22✔
386
                   preshared_key = excluded.preshared_key,
22✔
387
                   name = excluded.name,
22✔
388
                   email = excluded.email,
22✔
389
                   telegram_userid = excluded.telegram_userid,
22✔
390
                   subnet_ranges = excluded.subnet_ranges,
22✔
391
                   allocated_ips = excluded.allocated_ips,
22✔
392
                   allowed_ips = excluded.allowed_ips,
22✔
393
                   extra_allowed_ips = excluded.extra_allowed_ips,
22✔
394
                   endpoint = excluded.endpoint,
22✔
395
                   additional_notes = excluded.additional_notes,
22✔
396
                   use_server_dns = excluded.use_server_dns,
22✔
397
                   enabled = excluded.enabled,
22✔
398
                   updated_at = excluded.updated_at`,
22✔
399
                client.ID, client.PrivateKey, client.PublicKey, client.PresharedKey,
22✔
400
                client.Name, client.Email, client.TgUserid,
22✔
401
                string(subnetJSON), string(allocJSON), string(allowJSON), string(extraJSON),
22✔
402
                client.Endpoint, client.AdditionalNotes, client.UseServerDNS, client.Enabled,
22✔
403
                client.CreatedAt, client.UpdatedAt,
22✔
404
        )
22✔
405
        if err != nil {
22✔
NEW
406
                util.RemoveTgToClientID(client.ID)
×
NEW
407
                return err
×
NEW
408
        }
×
409

410
        // update telegram cache
411
        if client.Enabled && len(client.TgUserid) > 0 {
24✔
412
                if userid, err := strconv.ParseInt(client.TgUserid, 10, 64); err == nil {
4✔
413
                        util.UpdateTgToClientID(userid, client.ID)
2✔
414
                }
2✔
415
        } else {
20✔
416
                util.RemoveTgToClientID(client.ID)
20✔
417
        }
20✔
418
        return nil
22✔
419
}
420

421
// DeleteClient removes a client by ID
422
func (o *SqliteDB) DeleteClient(clientID string) error {
1✔
423
        util.RemoveTgToClientID(clientID)
1✔
424
        _, err := o.db.Exec("DELETE FROM clients WHERE id = ?", clientID)
1✔
425
        return err
1✔
426
}
1✔
427

428
// SaveServerInterface updates the server interface config
429
func (o *SqliteDB) SaveServerInterface(serverInterface model.ServerInterface) error {
2✔
430
        addrJSON, _ := json.Marshal(serverInterface.Addresses)
2✔
431
        _, err := o.db.Exec(
2✔
432
                `UPDATE server_interface SET addresses = ?, listen_port = ?, post_up = ?, pre_down = ?, post_down = ?, updated_at = ? WHERE id = 1`,
2✔
433
                string(addrJSON), serverInterface.ListenPort, serverInterface.PostUp, serverInterface.PreDown, serverInterface.PostDown, serverInterface.UpdatedAt,
2✔
434
        )
2✔
435
        return err
2✔
436
}
2✔
437

438
// SaveServerKeyPair updates the server keypair
439
func (o *SqliteDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error {
1✔
440
        _, err := o.db.Exec(
1✔
441
                `UPDATE server_keypair SET private_key = ?, public_key = ?, updated_at = ? WHERE id = 1`,
1✔
442
                serverKeyPair.PrivateKey, serverKeyPair.PublicKey, serverKeyPair.UpdatedAt,
1✔
443
        )
1✔
444
        return err
1✔
445
}
1✔
446

447
// SaveGlobalSettings updates global settings
448
func (o *SqliteDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error {
1✔
449
        dnsJSON, _ := json.Marshal(globalSettings.DNSServers)
1✔
450
        _, err := o.db.Exec(
1✔
451
                `UPDATE global_settings SET endpoint_address = ?, dns_servers = ?, mtu = ?, persistent_keepalive = ?,
1✔
452
                 firewall_mark = ?, "table" = ?, config_file_path = ?, updated_at = ? WHERE id = 1`,
1✔
453
                globalSettings.EndpointAddress, string(dnsJSON), globalSettings.MTU, globalSettings.PersistentKeepalive,
1✔
454
                globalSettings.FirewallMark, globalSettings.Table, globalSettings.ConfigFilePath, globalSettings.UpdatedAt,
1✔
455
        )
1✔
456
        return err
1✔
457
}
1✔
458

459
// GetAllocatedIPs returns all IP addresses allocated to clients and server
460
func (o *SqliteDB) GetAllocatedIPs(excludeClientID string) ([]string, error) {
2✔
461
        allocatedIPs := make([]string, 0)
2✔
462

2✔
463
        // server addresses
2✔
464
        var addrJSON string
2✔
465
        err := o.db.QueryRow("SELECT addresses FROM server_interface WHERE id = 1").Scan(&addrJSON)
2✔
466
        if err != nil {
2✔
NEW
467
                return nil, err
×
NEW
468
        }
×
469
        var serverAddrs []string
2✔
470
        json.Unmarshal([]byte(addrJSON), &serverAddrs)
2✔
471
        for _, cidr := range serverAddrs {
4✔
472
                ip, _, err := net.ParseCIDR(cidr)
2✔
473
                if err != nil {
2✔
NEW
474
                        return nil, err
×
NEW
475
                }
×
476
                allocatedIPs = append(allocatedIPs, ip.String())
2✔
477
        }
478

479
        // client addresses
480
        rows, err := o.db.Query("SELECT allocated_ips FROM clients WHERE id != ?", excludeClientID)
2✔
481
        if err != nil {
2✔
NEW
482
                return nil, err
×
NEW
483
        }
×
484
        defer rows.Close()
2✔
485
        for rows.Next() {
4✔
486
                var ipsJSON string
2✔
487
                if err := rows.Scan(&ipsJSON); err != nil {
2✔
NEW
488
                        return nil, err
×
NEW
489
                }
×
490
                var ips []string
2✔
491
                json.Unmarshal([]byte(ipsJSON), &ips)
2✔
492
                for _, cidr := range ips {
4✔
493
                        ip, _, err := net.ParseCIDR(cidr)
2✔
494
                        if err != nil {
2✔
NEW
495
                                return nil, err
×
NEW
496
                        }
×
497
                        allocatedIPs = append(allocatedIPs, ip.String())
2✔
498
                }
499
        }
500

501
        return allocatedIPs, rows.Err()
2✔
502
}
503

504
// GetPath returns the database file path
505
func (o *SqliteDB) GetPath() string {
1✔
506
        return filepath.Dir(o.dbPath)
1✔
507
}
1✔
508

509
// GetHashes returns stored hashes
510
func (o *SqliteDB) GetHashes() (model.ClientServerHashes, error) {
7✔
511
        var h model.ClientServerHashes
7✔
512
        err := o.db.QueryRow("SELECT client, server FROM hashes WHERE id = 1").Scan(&h.Client, &h.Server)
7✔
513
        return h, err
7✔
514
}
7✔
515

516
// SaveHashes updates stored hashes
517
func (o *SqliteDB) SaveHashes(hashes model.ClientServerHashes) error {
3✔
518
        _, err := o.db.Exec("UPDATE hashes SET client = ?, server = ? WHERE id = 1", hashes.Client, hashes.Server)
3✔
519
        return err
3✔
520
}
3✔
521

522
// DB returns the underlying sql.DB for direct access (e.g., audit logs)
523
func (o *SqliteDB) DB() *sql.DB {
1✔
524
        return o.db
1✔
525
}
1✔
526

527
// GetUserByOIDCSub returns a user by their OIDC subject identifier
528
func (o *SqliteDB) GetUserByOIDCSub(sub string) (model.User, error) {
3✔
529
        var u model.User
3✔
530
        err := o.db.QueryRow(
3✔
531
                "SELECT "+userColumns+" FROM users WHERE oidc_sub = ?", sub,
3✔
532
        ).Scan(&u.Username, &u.Email, &u.DisplayName, &u.OIDCSub, &u.Admin, &u.CreatedAt, &u.UpdatedAt)
3✔
533
        return u, err
3✔
534
}
3✔
535

536
// scanner is satisfied by both *sql.Rows and *sql.Row
537
type scanner interface {
538
        Scan(dest ...interface{}) error
539
}
540

541
func scanClientFrom(s scanner) (model.Client, error) {
17✔
542
        var c model.Client
17✔
543
        var subnetJSON, allocJSON, allowJSON, extraJSON string
17✔
544
        err := s.Scan(
17✔
545
                &c.ID, &c.PrivateKey, &c.PublicKey, &c.PresharedKey,
17✔
546
                &c.Name, &c.Email, &c.TgUserid,
17✔
547
                &subnetJSON, &allocJSON, &allowJSON, &extraJSON,
17✔
548
                &c.Endpoint, &c.AdditionalNotes, &c.UseServerDNS, &c.Enabled,
17✔
549
                &c.CreatedAt, &c.UpdatedAt,
17✔
550
        )
17✔
551
        if err != nil {
19✔
552
                return c, err
2✔
553
        }
2✔
554
        json.Unmarshal([]byte(subnetJSON), &c.SubnetRanges)
15✔
555
        json.Unmarshal([]byte(allocJSON), &c.AllocatedIPs)
15✔
556
        json.Unmarshal([]byte(allowJSON), &c.AllowedIPs)
15✔
557
        json.Unmarshal([]byte(extraJSON), &c.ExtraAllowedIPs)
15✔
558
        return c, nil
15✔
559
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc