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

DigitalTolk / wireguard-ui / 24847022216

23 Apr 2026 04:37PM UTC coverage: 81.636% (-0.7%) from 82.32%
24847022216

Pull #16

github

web-flow
Merge ddd9d4228 into 0c50253d1
Pull Request #16: Fix auth

462 of 590 branches covered (78.31%)

Branch coverage included in aggregate %.

38 of 74 new or added lines in 5 files covered. (51.35%)

201 existing lines in 8 files now uncovered.

2832 of 3445 relevant lines covered (82.21%)

14.17 hits per line

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

87.44
/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
        "time"
13

14
        _ "modernc.org/sqlite"
15

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

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

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

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

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

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

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

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

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

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

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

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

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

137
        // Clean up legacy password users that were migrated from JSON but have no OIDC subject.
138
        // These users can never log in with SSO-only auth, and their presence prevents
139
        // the first OIDC login from being auto-promoted to admin.
140
        o.db.Exec(`DELETE FROM users WHERE oidc_sub IS NULL OR oidc_sub = ''`)
29✔
141

29✔
142
        // init caches (first OIDC login auto-provisions admin user)
29✔
143
        users, err := o.GetUsers()
29✔
144
        if err == nil {
58✔
145
                util.DBUsersToCRC32Mutex.Lock()
29✔
146
                for _, user := range users {
29✔
UNCOV
147
                        util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
×
UNCOV
148
                }
×
149
                util.DBUsersToCRC32Mutex.Unlock()
29✔
150
        }
151

152
        return nil
29✔
153
}
154

155
// GetUsers returns all users
156
func (o *SqliteDB) GetUsers() ([]model.User, error) {
31✔
157
        rows, err := o.db.Query("SELECT " + userColumns + " FROM users")
31✔
158
        if err != nil {
31✔
UNCOV
159
                return nil, err
×
UNCOV
160
        }
×
161
        defer rows.Close()
31✔
162

31✔
163
        var users []model.User
31✔
164
        for rows.Next() {
33✔
165
                var u model.User
2✔
166
                if err := rows.Scan(&u.Username, &u.Email, &u.DisplayName, &u.OIDCSub, &u.Admin, &u.CreatedAt, &u.UpdatedAt); err != nil {
2✔
UNCOV
167
                        return nil, err
×
UNCOV
168
                }
×
169
                users = append(users, u)
2✔
170
        }
171
        return users, rows.Err()
31✔
172
}
173

174
// GetUserByName returns a single user by username
175
func (o *SqliteDB) GetUserByName(username string) (model.User, error) {
7✔
176
        var u model.User
7✔
177
        err := o.db.QueryRow(
7✔
178
                "SELECT "+userColumns+" FROM users WHERE username = ?",
7✔
179
                username,
7✔
180
        ).Scan(&u.Username, &u.Email, &u.DisplayName, &u.OIDCSub, &u.Admin, &u.CreatedAt, &u.UpdatedAt)
7✔
181
        if err != nil {
10✔
182
                return u, err
3✔
183
        }
3✔
184
        return u, nil
4✔
185
}
186

187
// SaveUser creates or updates a user
188
func (o *SqliteDB) SaveUser(user model.User) error {
10✔
189
        now := time.Now().UTC()
10✔
190
        if user.UpdatedAt.IsZero() {
11✔
191
                user.UpdatedAt = now
1✔
192
        }
1✔
193
        if user.CreatedAt.IsZero() {
11✔
194
                user.CreatedAt = now
1✔
195
        }
1✔
196

197
        _, err := o.db.Exec(
10✔
198
                `INSERT INTO users (username, email, display_name, oidc_sub, admin, created_at, updated_at)
10✔
199
                 VALUES (?, ?, ?, NULLIF(?, ''), ?, ?, ?)
10✔
200
                 ON CONFLICT(username) DO UPDATE SET
10✔
201
                   email = excluded.email,
10✔
202
                   display_name = excluded.display_name,
10✔
203
                   oidc_sub = excluded.oidc_sub,
10✔
204
                   admin = excluded.admin,
10✔
205
                   updated_at = excluded.updated_at`,
10✔
206
                user.Username, user.Email, user.DisplayName, user.OIDCSub, user.Admin, user.CreatedAt, user.UpdatedAt,
10✔
207
        )
10✔
208
        if err != nil {
10✔
UNCOV
209
                return err
×
UNCOV
210
        }
×
211
        util.DBUsersToCRC32Mutex.Lock()
10✔
212
        util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
10✔
213
        util.DBUsersToCRC32Mutex.Unlock()
10✔
214
        return nil
10✔
215
}
216

217
// DeleteUser removes a user by username
218
func (o *SqliteDB) DeleteUser(username string) error {
1✔
219
        util.DBUsersToCRC32Mutex.Lock()
1✔
220
        delete(util.DBUsersToCRC32, username)
1✔
221
        util.DBUsersToCRC32Mutex.Unlock()
1✔
222
        _, err := o.db.Exec("DELETE FROM users WHERE username = ?", username)
1✔
223
        return err
1✔
224
}
1✔
225

226
// GetGlobalSettings returns global WireGuard settings
227
func (o *SqliteDB) GetGlobalSettings() (model.GlobalSetting, error) {
16✔
228
        var gs model.GlobalSetting
16✔
229
        var dnsJSON string
16✔
230
        err := o.db.QueryRow(
16✔
231
                `SELECT endpoint_address, dns_servers, mtu, persistent_keepalive, firewall_mark, "table", config_file_path, updated_at
16✔
232
                 FROM global_settings WHERE id = 1`,
16✔
233
        ).Scan(&gs.EndpointAddress, &dnsJSON, &gs.MTU, &gs.PersistentKeepalive, &gs.FirewallMark, &gs.Table, &gs.ConfigFilePath, &gs.UpdatedAt)
16✔
234
        if err != nil {
16✔
UNCOV
235
                return gs, err
×
UNCOV
236
        }
×
237
        json.Unmarshal([]byte(dnsJSON), &gs.DNSServers)
16✔
238
        return gs, nil
16✔
239
}
240

241
// GetServer returns the server config (interface + keypair)
242
func (o *SqliteDB) GetServer() (model.Server, error) {
19✔
243
        server := model.Server{}
19✔
244

19✔
245
        // interface
19✔
246
        iface := model.ServerInterface{}
19✔
247
        var addrJSON string
19✔
248
        err := o.db.QueryRow(
19✔
249
                "SELECT addresses, listen_port, post_up, pre_down, post_down, updated_at FROM server_interface WHERE id = 1",
19✔
250
        ).Scan(&addrJSON, &iface.ListenPort, &iface.PostUp, &iface.PreDown, &iface.PostDown, &iface.UpdatedAt)
19✔
251
        if err != nil {
19✔
UNCOV
252
                return server, fmt.Errorf("cannot read server interface: %w", err)
×
UNCOV
253
        }
×
254
        json.Unmarshal([]byte(addrJSON), &iface.Addresses)
19✔
255
        server.Interface = &iface
19✔
256

19✔
257
        // keypair
19✔
258
        kp := model.ServerKeypair{}
19✔
259
        err = o.db.QueryRow(
19✔
260
                "SELECT private_key, public_key, updated_at FROM server_keypair WHERE id = 1",
19✔
261
        ).Scan(&kp.PrivateKey, &kp.PublicKey, &kp.UpdatedAt)
19✔
262
        if err != nil {
19✔
UNCOV
263
                return server, fmt.Errorf("cannot read server keypair: %w", err)
×
UNCOV
264
        }
×
265
        server.KeyPair = &kp
19✔
266

19✔
267
        return server, nil
19✔
268
}
269

270
// GetClients returns all clients, optionally with QR codes
271
func (o *SqliteDB) GetClients(hasQRCode bool) ([]model.ClientData, error) {
11✔
272
        rows, err := o.db.Query(
11✔
273
                `SELECT id, private_key, public_key, preshared_key, name, email,
11✔
274
                        subnet_ranges, allocated_ips, allowed_ips, extra_allowed_ips,
11✔
275
                        endpoint, additional_notes, use_server_dns, enabled, created_at, updated_at
11✔
276
                 FROM clients`,
11✔
277
        )
11✔
278
        if err != nil {
11✔
UNCOV
279
                return nil, err
×
UNCOV
280
        }
×
281
        defer rows.Close()
11✔
282

11✔
283
        // fetch server/settings once outside the loop for QR generation
11✔
284
        var server model.Server
11✔
285
        var globalSettings model.GlobalSetting
11✔
286
        if hasQRCode {
13✔
287
                server, _ = o.GetServer()
2✔
288
                globalSettings, _ = o.GetGlobalSettings()
2✔
289
        }
2✔
290

291
        var clients []model.ClientData
11✔
292
        for rows.Next() {
18✔
293
                client, err := scanClientFrom(rows)
7✔
294
                if err != nil {
7✔
UNCOV
295
                        return nil, err
×
UNCOV
296
                }
×
297

298
                clientData := model.ClientData{Client: &client}
7✔
299

7✔
300
                if hasQRCode && client.PrivateKey != "" {
8✔
301
                        png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256)
1✔
302
                        if err == nil {
2✔
303
                                clientData.QRCode = qrCodeDataURIPrefix + base64.StdEncoding.EncodeToString(png)
1✔
304
                        }
1✔
305
                }
306

307
                clients = append(clients, clientData)
7✔
308
        }
309
        return clients, rows.Err()
11✔
310
}
311

312
// GetClientByID returns a single client by ID
313
func (o *SqliteDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSettings) (model.ClientData, error) {
10✔
314
        clientData := model.ClientData{}
10✔
315

10✔
316
        row := o.db.QueryRow(
10✔
317
                `SELECT id, private_key, public_key, preshared_key, name, email,
10✔
318
                        subnet_ranges, allocated_ips, allowed_ips, extra_allowed_ips,
10✔
319
                        endpoint, additional_notes, use_server_dns, enabled, created_at, updated_at
10✔
320
                 FROM clients WHERE id = ?`, clientID,
10✔
321
        )
10✔
322

10✔
323
        client, err := scanClientFrom(row)
10✔
324
        if err != nil {
12✔
325
                return clientData, err
2✔
326
        }
2✔
327

328
        if qrCodeSettings.Enabled && client.PrivateKey != "" {
10✔
329
                server, _ := o.GetServer()
2✔
330
                globalSettings, _ := o.GetGlobalSettings()
2✔
331
                if !qrCodeSettings.IncludeDNS {
3✔
332
                        globalSettings.DNSServers = []string{}
1✔
333
                }
1✔
334
                if !qrCodeSettings.IncludeMTU {
3✔
335
                        globalSettings.MTU = 0
1✔
336
                }
1✔
337
                png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256)
2✔
338
                if err == nil {
4✔
339
                        clientData.QRCode = qrCodeDataURIPrefix + base64.StdEncoding.EncodeToString(png)
2✔
340
                }
2✔
341
        }
342

343
        clientData.Client = &client
8✔
344
        return clientData, nil
8✔
345
}
346

347
// SaveClient creates or updates a client
348
func (o *SqliteDB) SaveClient(client model.Client) error {
19✔
349
        subnetJSON, _ := json.Marshal(client.SubnetRanges)
19✔
350
        allocJSON, _ := json.Marshal(client.AllocatedIPs)
19✔
351
        allowJSON, _ := json.Marshal(client.AllowedIPs)
19✔
352
        extraJSON, _ := json.Marshal(client.ExtraAllowedIPs)
19✔
353

19✔
354
        _, err := o.db.Exec(
19✔
355
                `INSERT INTO clients (id, private_key, public_key, preshared_key, name, email,
19✔
356
                                      subnet_ranges, allocated_ips, allowed_ips, extra_allowed_ips,
19✔
357
                                      endpoint, additional_notes, use_server_dns, enabled, created_at, updated_at)
19✔
358
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
19✔
359
                 ON CONFLICT(id) DO UPDATE SET
19✔
360
                   private_key = excluded.private_key,
19✔
361
                   public_key = excluded.public_key,
19✔
362
                   preshared_key = excluded.preshared_key,
19✔
363
                   name = excluded.name,
19✔
364
                   email = excluded.email,
19✔
365
                   subnet_ranges = excluded.subnet_ranges,
19✔
366
                   allocated_ips = excluded.allocated_ips,
19✔
367
                   allowed_ips = excluded.allowed_ips,
19✔
368
                   extra_allowed_ips = excluded.extra_allowed_ips,
19✔
369
                   endpoint = excluded.endpoint,
19✔
370
                   additional_notes = excluded.additional_notes,
19✔
371
                   use_server_dns = excluded.use_server_dns,
19✔
372
                   enabled = excluded.enabled,
19✔
373
                   updated_at = excluded.updated_at`,
19✔
374
                client.ID, client.PrivateKey, client.PublicKey, client.PresharedKey,
19✔
375
                client.Name, client.Email,
19✔
376
                string(subnetJSON), string(allocJSON), string(allowJSON), string(extraJSON),
19✔
377
                client.Endpoint, client.AdditionalNotes, client.UseServerDNS, client.Enabled,
19✔
378
                client.CreatedAt, client.UpdatedAt,
19✔
379
        )
19✔
380
        return err
19✔
381
}
19✔
382

383
// DeleteClient removes a client by ID
384
func (o *SqliteDB) DeleteClient(clientID string) error {
1✔
385
        _, err := o.db.Exec("DELETE FROM clients WHERE id = ?", clientID)
1✔
386
        return err
1✔
387
}
1✔
388

389
// SaveServerInterface updates the server interface config
390
func (o *SqliteDB) SaveServerInterface(serverInterface model.ServerInterface) error {
2✔
391
        addrJSON, _ := json.Marshal(serverInterface.Addresses)
2✔
392
        _, err := o.db.Exec(
2✔
393
                `UPDATE server_interface SET addresses = ?, listen_port = ?, post_up = ?, pre_down = ?, post_down = ?, updated_at = ? WHERE id = 1`,
2✔
394
                string(addrJSON), serverInterface.ListenPort, serverInterface.PostUp, serverInterface.PreDown, serverInterface.PostDown, serverInterface.UpdatedAt,
2✔
395
        )
2✔
396
        return err
2✔
397
}
2✔
398

399
// SaveServerKeyPair updates the server keypair
400
func (o *SqliteDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error {
1✔
401
        _, err := o.db.Exec(
1✔
402
                `UPDATE server_keypair SET private_key = ?, public_key = ?, updated_at = ? WHERE id = 1`,
1✔
403
                serverKeyPair.PrivateKey, serverKeyPair.PublicKey, serverKeyPair.UpdatedAt,
1✔
404
        )
1✔
405
        return err
1✔
406
}
1✔
407

408
// SaveGlobalSettings updates global settings
409
func (o *SqliteDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error {
1✔
410
        dnsJSON, _ := json.Marshal(globalSettings.DNSServers)
1✔
411
        _, err := o.db.Exec(
1✔
412
                `UPDATE global_settings SET endpoint_address = ?, dns_servers = ?, mtu = ?, persistent_keepalive = ?,
1✔
413
                 firewall_mark = ?, "table" = ?, config_file_path = ?, updated_at = ? WHERE id = 1`,
1✔
414
                globalSettings.EndpointAddress, string(dnsJSON), globalSettings.MTU, globalSettings.PersistentKeepalive,
1✔
415
                globalSettings.FirewallMark, globalSettings.Table, globalSettings.ConfigFilePath, globalSettings.UpdatedAt,
1✔
416
        )
1✔
417
        return err
1✔
418
}
1✔
419

420
// GetAllocatedIPs returns all IP addresses allocated to clients and server
421
func (o *SqliteDB) GetAllocatedIPs(excludeClientID string) ([]string, error) {
2✔
422
        allocatedIPs := make([]string, 0)
2✔
423

2✔
424
        // server addresses
2✔
425
        var addrJSON string
2✔
426
        err := o.db.QueryRow("SELECT addresses FROM server_interface WHERE id = 1").Scan(&addrJSON)
2✔
427
        if err != nil {
2✔
UNCOV
428
                return nil, err
×
UNCOV
429
        }
×
430
        var serverAddrs []string
2✔
431
        json.Unmarshal([]byte(addrJSON), &serverAddrs)
2✔
432
        for _, cidr := range serverAddrs {
4✔
433
                ip, _, err := net.ParseCIDR(cidr)
2✔
434
                if err != nil {
2✔
UNCOV
435
                        return nil, err
×
UNCOV
436
                }
×
437
                allocatedIPs = append(allocatedIPs, ip.String())
2✔
438
        }
439

440
        // client addresses
441
        rows, err := o.db.Query("SELECT allocated_ips FROM clients WHERE id != ?", excludeClientID)
2✔
442
        if err != nil {
2✔
UNCOV
443
                return nil, err
×
UNCOV
444
        }
×
445
        defer rows.Close()
2✔
446
        for rows.Next() {
4✔
447
                var ipsJSON string
2✔
448
                if err := rows.Scan(&ipsJSON); err != nil {
2✔
UNCOV
449
                        return nil, err
×
UNCOV
450
                }
×
451
                var ips []string
2✔
452
                json.Unmarshal([]byte(ipsJSON), &ips)
2✔
453
                for _, cidr := range ips {
4✔
454
                        ip, _, err := net.ParseCIDR(cidr)
2✔
455
                        if err != nil {
2✔
UNCOV
456
                                return nil, err
×
UNCOV
457
                        }
×
458
                        allocatedIPs = append(allocatedIPs, ip.String())
2✔
459
                }
460
        }
461

462
        return allocatedIPs, rows.Err()
2✔
463
}
464

465
// GetPath returns the database file path
466
func (o *SqliteDB) GetPath() string {
1✔
467
        return filepath.Dir(o.dbPath)
1✔
468
}
1✔
469

470
// GetHashes returns stored hashes
471
func (o *SqliteDB) GetHashes() (model.ClientServerHashes, error) {
7✔
472
        var h model.ClientServerHashes
7✔
473
        err := o.db.QueryRow("SELECT client, server FROM hashes WHERE id = 1").Scan(&h.Client, &h.Server)
7✔
474
        return h, err
7✔
475
}
7✔
476

477
// SaveHashes updates stored hashes
478
func (o *SqliteDB) SaveHashes(hashes model.ClientServerHashes) error {
3✔
479
        _, err := o.db.Exec("UPDATE hashes SET client = ?, server = ? WHERE id = 1", hashes.Client, hashes.Server)
3✔
480
        return err
3✔
481
}
3✔
482

483
// DB returns the underlying sql.DB for direct access (e.g., audit logs)
484
func (o *SqliteDB) DB() *sql.DB {
1✔
485
        return o.db
1✔
486
}
1✔
487

488
// GetUserByOIDCSub returns a user by their OIDC subject identifier
489
func (o *SqliteDB) GetUserByOIDCSub(sub string) (model.User, error) {
3✔
490
        var u model.User
3✔
491
        err := o.db.QueryRow(
3✔
492
                "SELECT "+userColumns+" FROM users WHERE oidc_sub = ?", sub,
3✔
493
        ).Scan(&u.Username, &u.Email, &u.DisplayName, &u.OIDCSub, &u.Admin, &u.CreatedAt, &u.UpdatedAt)
3✔
494
        return u, err
3✔
495
}
3✔
496

497
// scanner is satisfied by both *sql.Rows and *sql.Row
498
type scanner interface {
499
        Scan(dest ...interface{}) error
500
}
501

502
func scanClientFrom(s scanner) (model.Client, error) {
17✔
503
        var c model.Client
17✔
504
        var subnetJSON, allocJSON, allowJSON, extraJSON string
17✔
505
        err := s.Scan(
17✔
506
                &c.ID, &c.PrivateKey, &c.PublicKey, &c.PresharedKey,
17✔
507
                &c.Name, &c.Email,
17✔
508
                &subnetJSON, &allocJSON, &allowJSON, &extraJSON,
17✔
509
                &c.Endpoint, &c.AdditionalNotes, &c.UseServerDNS, &c.Enabled,
17✔
510
                &c.CreatedAt, &c.UpdatedAt,
17✔
511
        )
17✔
512
        if err != nil {
19✔
513
                return c, err
2✔
514
        }
2✔
515
        json.Unmarshal([]byte(subnetJSON), &c.SubnetRanges)
15✔
516
        json.Unmarshal([]byte(allocJSON), &c.AllocatedIPs)
15✔
517
        json.Unmarshal([]byte(allowJSON), &c.AllowedIPs)
15✔
518
        json.Unmarshal([]byte(extraJSON), &c.ExtraAllowedIPs)
15✔
519
        return c, nil
15✔
520
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc